11
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GAS で CPaaS NOW のAPIに接続してみた

Posted at

概要

  • CPaaS NOWをGAS(Google Apps Script)で接続してみようと思い生成AI(Github copilot Agent)を利用して作ってみた

このアプリの仕様

  • spreadsheetにある連絡先に対してSMSを一括で送ることができる
  • CPaaS NOWの「SMS配信登録」と「SMS配信結果取得」のAPIを利用
  • 送信に必要な情報は相手先の電話番号のみ
  • SMS送信するメッセージは固定文言
  • 送信結果は別シートで確認することができる

やり方

CPaaS NOWのAPIドキュメントからopenapi.yamlファイルをダウンロードする

CPaaS_NOW_API_ドキュメント.png (73.0 kB)

VS Codeでダウンロードしたyamlファイルを開きAIエージェント(Github copilot Agent)に依頼する

Google Apps ScriptでCPaaS NOWに接続するツールを作成してみたい、どう始めればいい?
  • 依頼したらあとはひたすら待つだけ :coffee:

出来上がったファイルでやり方を確認する

  • sms送信以外の部分はカットしています
readme.md
# Google Apps Script用 CPaaS NOW サンプル

このディレクトリには、Google Apps ScriptでCPaaS NOWのAPIを利用するためのサンプルコードとドキュメントが含まれています。

## 開始方法

### 1. Google Apps Scriptプロジェクトの作成

1. [Google Apps Script](https://script.google.com/) にアクセス
2. 「新しいプロジェクト」をクリック
3. プロジェクト名を設定(例:「CPaaS NOW連携ツール」)

### 2. 基本設定

Google Apps ScriptでCPaaS NOWを利用するには、以下の手順で設定を行います:

1. **APIトークンの設定**
   - CPaaS NOWの設定画面からAPIトークンを取得
   - Google Apps Scriptの「プロジェクトの設定」→「スクリプト プロパティ」でAPIトークンを設定

2. **HTTPSリクエストの有効化**
   - Google Apps Scriptでは外部APIへのHTTPSリクエストがデフォルトで利用可能

### 3. サンプルファイル

- `sms_basic.gs` - SMS配信の基本例
- `spreadsheet_integration.gs` - Googleスプレッドシートとの連携例
- `cpaas_client.gs` - CPaaS NOW APIクライアントクラス

~~~省略

実際にやってみる

1.スプレッドシートでGASを開く

スクリーンショット_2025-07-22_9_22_05.png (100.3 kB)

2.GASのスクリプトに下記ファイルを追加する

スクリーンショット_2025-07-22_17_14_40.png (108.0 kB)
  • cpaas_client.gs
  • spreadsheet.gs
cpaas_client.gs
/**
 * CPaaS NOW API クライアントクラス
 * Google Apps Scriptで利用するためのCPaaS NOW APIラッパー
 */
class CPaaSClient {
  constructor(apiToken, baseUrl = 'https://sandbox.cpaasnow.com') {
    this.apiToken = apiToken;
    this.baseUrl = baseUrl;
  }

  /**
   * 共通のHTTPリクエスト処理
   * @param {string} endpoint - APIエンドポイント
   * @param {string} method - HTTPメソッド
   * @param {Object} payload - リクエストボディ
   * @returns {Object} APIレスポンス
   */
  makeRequest(endpoint, method = 'GET', payload = null) {
    const options = {
      method: method,
      headers: {
        'Authorization': `Bearer ${this.apiToken}`,
        'Content-Type': 'application/json'
      }
    };

    if (payload && method !== 'GET') {
      options.payload = JSON.stringify(payload);
    }

    try {
      const response = UrlFetchApp.fetch(`${this.baseUrl}${endpoint}`, options);
      const responseCode = response.getResponseCode();
      const responseText = response.getContentText();

      // レスポンスコードチェック
      if (responseCode >= 200 && responseCode < 300) {
        return {
          success: true,
          data: JSON.parse(responseText),
          status: responseCode
        };
      } else {
        const errorData = JSON.parse(responseText);
        return {
          success: false,
          error: errorData,
          status: responseCode
        };
      }
    } catch (error) {
      console.error('API Request Error:', error);
      return {
        success: false,
        error: { message: error.toString() },
        status: 0
      };
    }
  }

  /**
   * SMS配信登録
   * @param {string} phoneNumber - 宛先電話番号
   * @param {string} message - SMS本文
   * @param {boolean} clickTracking - クリックトラッキング有効/無効
   * @param {string} userReference - ユーザー参照情報
   * @param {string} billSplitCode - 請求分割コード
   * @returns {Object} API レスポンス
   */
  sendSMS(phoneNumber, message, clickTracking = false, userReference = null, billSplitCode = null) {
    const payload = {
      to: phoneNumber,
      text: message,
      click_tracking: clickTracking
    };

    if (userReference) payload.user_reference = userReference;
    if (billSplitCode) payload.bill_split_code = billSplitCode;

    return this.makeRequest('/api/v1/short_messages', 'POST', payload);
  }

  /**
   * SMS配信結果取得
   * @param {string} deliveryOrderIds - 配信オーダーID(カンマ区切り)
   * @param {string} acceptedAtFrom - 受付日時開始
   * @param {string} acceptedAtTo - 受付日時終了
   * @param {number} limit - 取得件数制限
   * @param {number} offset - オフセット
   * @returns {Object} API レスポンス
   */
  getSMSResults(deliveryOrderIds = null, acceptedAtFrom = null, acceptedAtTo = null, limit = 100, offset = 0) {
    let params = [`limit=${limit}`, `offset=${offset}`];

    if (deliveryOrderIds) params.push(`delivery_order_ids=${deliveryOrderIds}`);
    if (acceptedAtFrom) params.push(`accepted_at_from=${acceptedAtFrom}`);
    if (acceptedAtTo) params.push(`accepted_at_to=${acceptedAtTo}`);

    const queryString = params.join('&');
    return this.makeRequest(`/api/v1/short_messages?${queryString}`, 'GET');
  }

spreadsheet.gs
/**
 * Googleスプレッドシートとの連携例
 *
 * 機能:
 * - スプレッドシートから連絡先を読み込み
 * - 一括配信の実行
 * - 配信結果の記録
 * - 配信ログの管理
 *
 * 前提条件:
 * 1. スクリプト プロパティに「CPAAS_API_TOKEN」を設定
 * 2. cpaas_client.gs ファイルを同じプロジェクトに追加
 */

/**
 * メイン処理:スプレッドシートから連絡先を読み込んでSMS一括配信
 */
function sendSMSFromSpreadsheet() {
  try {
    const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
    const contactsSheet = getOrCreateSheet(spreadsheet, '連絡先');
    const logsSheet = getOrCreateSheet(spreadsheet, '配信ログ');

    // 初回実行時にヘッダーを作成
    setupContactsSheet(contactsSheet);
    setupLogsSheet(logsSheet);

    // 連絡先データを読み込み
    const contacts = readContactsFromSheet(contactsSheet);
    if (contacts.length === 0) {
      console.log('配信対象の連絡先がありません');
      return;
    }

    console.log(`${contacts.length}件の連絡先を読み込みました`);

    // CPaaS NOW クライアントを初期化
    const apiToken = PropertiesService.getScriptProperties().getProperty('CPAAS_API_TOKEN');
    if (!apiToken) {
      throw new Error('CPAAS_API_TOKEN が設定されていません。setup_guide.gs  setupAPIToken() を実行してください。');
    }
    const client = new CPaaSClient(apiToken);

    // メッセージ内容(設定シートから読み込むことも可能)
    const message = getMessageTemplate();

    // 一括配信実行
    const results = [];
    for (let i = 0; i < contacts.length; i++) {
      const contact = contacts[i];
      console.log(`配信中: ${contact.name} (${i + 1}/${contacts.length})`);

      // 送信前の最終チェック
      if (!contact.phoneNumber || contact.phoneNumber === '') {
        console.error(`エラー: ${contact.name}の電話番号が空です`);
        const logEntry = {
          timestamp: new Date(),
          name: contact.name,
          phoneNumber: contact.phoneNumber || '',
          success: false,
          deliveryOrderId: '',
          errorCode: 'MISSING_PHONE_NUMBER',
          errorMessage: '電話番号が入力されていません'
        };
        results.push(logEntry);
        writeLogToSheet(logsSheet, logEntry);
        continue;
      }

      console.log(`→ 送信先: ${contact.phoneNumber}`);

      const result = client.sendSMS(
        contact.phoneNumber,
        message,
        false, // クリックトラッキング
        `batch_${new Date().getTime()}_${i + 1}` // ユーザー参照
      );

      console.log(`配信結果: ${result.success ? '成功' : '失敗'}`);
      if (!result.success) {
        console.error(`エラー詳細: ${result.error.code} - ${result.error.message}`);
      }

      // 結果を記録
      const logEntry = {
        timestamp: new Date(),
        name: contact.name,
        phoneNumber: contact.phoneNumber,
        success: result.success,
        deliveryOrderId: result.success ? result.data.delivery_order_id : '',
        errorCode: result.success ? '' : result.error.code,
        errorMessage: result.success ? '' : result.error.message
      };

      results.push(logEntry);

      // ログシートに記録
      writeLogToSheet(logsSheet, logEntry);

      // レート制限対策
      Utilities.sleep(1000);
    }

    // 結果サマリーを表示
    showResultSummary(results);

  } catch (error) {
    console.error('エラーが発生しました:', error);
    throw error;
  }
}

// ===== ヘルパー関数 =====

/**
 * シートを取得または作成
 */
function getOrCreateSheet(spreadsheet, sheetName) {
  console.log(`getOrCreateSheet: シート "${sheetName}" を取得または作成中...`);

  if (!spreadsheet) {
    console.error('エラー: spreadsheet  null または undefined です');
    throw new Error('spreadsheet パラメータが無効です');
  }

  console.log(`スプレッドシート名: ${spreadsheet.getName()}`);

  let sheet;
  try {
    sheet = spreadsheet.getSheetByName(sheetName);
    if (sheet) {
      console.log(`✓ 既存のシート "${sheetName}" を取得しました`);
    } else {
      console.log(`シート "${sheetName}" が存在しません。新規作成します...`);
      sheet = spreadsheet.insertSheet(sheetName);
      console.log(`✓ 新しいシート "${sheetName}" を作成しました`);
    }
  } catch (error) {
    console.error(`シート操作中にエラーが発生しました: ${error.toString()}`);
    throw error;
  }

  if (!sheet) {
    throw new Error(`シート "${sheetName}" の作成に失敗しました`);
  }

  return sheet;
}

/**
 * 連絡先シートのセットアップ
 */
function setupContactsSheet(sheet) {
  console.log('setupContactsSheet: 連絡先シートをセットアップ中...');

  if (!sheet) {
    console.error('エラー: sheet パラメータが null または undefined です');
    throw new Error('sheet パラメータが無効です');
  }

  console.log(`シート名: ${sheet.getName()}`);
  console.log(`現在の行数: ${sheet.getLastRow()}`);

  if (sheet.getLastRow() === 0) {
    console.log('空のシートです。ヘッダーとサンプルデータを追加します...');

    const headers = ['名前', '電話番号', 'メールアドレス', '備考'];
    sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
    sheet.getRange(1, 1, 1, headers.length).setFontWeight('bold');

    // 電話番号列を文字列フォーマットに設定
    const phoneNumberColumn = sheet.getRange('B:B'); // B列(電話番号列)
    phoneNumberColumn.setNumberFormat('@'); // @ は文字列フォーマット
    console.log('✓ 電話番号列を文字列フォーマットに設定しました');

    // サンプルデータを追加(文字列として明示的に設定)
    const sampleData = [
      ['田中太郎', '09001111101', 'success@example.com', 'テスト用'],
      ['佐藤花子', '09001111102', 'success@example.com', 'テスト用'],
      ['鈴木次郎', '09001111103', 'success@example.com', 'テスト用']
    ];

    // データを1行ずつ設定して文字列として保存
    for (let i = 0; i < sampleData.length; i++) {
      const row = sampleData[i];
      sheet.getRange(i + 2, 1).setValue(row[0]); // 名前
      sheet.getRange(i + 2, 2).setValue(row[1]); // 電話番号(文字列として)
      sheet.getRange(i + 2, 3).setValue(row[2]); // メール
      sheet.getRange(i + 2, 4).setValue(row[3]); // 備考
    }

    console.log('✓ 連絡先シートを初期化しました');
    console.log('注意: テスト用電話番号を使用しています。実際の配信前に適切な番号に変更してください。');
    console.log('📝 電話番号は文字列フォーマットで入力してください(先頭の0が保持されます)');
  } else {
    console.log('✓ 連絡先シートは既に初期化済みです');

    // 既存シートでも電話番号列のフォーマットを設定
    const phoneNumberColumn = sheet.getRange('B:B');
    phoneNumberColumn.setNumberFormat('@');
    console.log('✓ 既存の電話番号列を文字列フォーマットに設定しました');
  }
}

/**
 * ログシートのセットアップ
 */
function setupLogsSheet(sheet) {
  console.log('setupLogsSheet: ログシートをセットアップ中...');

  if (!sheet) {
    console.error('エラー: sheet パラメータが null または undefined です');
    throw new Error('sheet パラメータが無効です');
  }

  console.log(`シート名: ${sheet.getName()}`);
  console.log(`現在の行数: ${sheet.getLastRow()}`);

  if (sheet.getLastRow() === 0) {
    console.log('空のシートです。ヘッダーを追加します...');

    const headers = ['配信日時', '名前', '電話番号', 'メールアドレス', '配信オーダーID', '成功/失敗', 'エラーコード', 'エラーメッセージ'];
    sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
    sheet.getRange(1, 1, 1, headers.length).setFontWeight('bold');

    console.log('✓ ログシートを初期化しました');
  } else {
    console.log('✓ ログシートは既に初期化済みです');
  }
}

/**
 * 連絡先データの読み込み
 */
function readContactsFromSheet(sheet) {
  const data = sheet.getDataRange().getValues();
  const contacts = [];

  console.log(`シートから${data.length - 1}行のデータを読み込み中...`);

  for (let i = 1; i < data.length; i++) { // ヘッダー行をスキップ
    const name = data[i][0];
    const phoneNumber = data[i][1];
    const email = data[i][2];
    const note = data[i][3];

    // デバッグ用ログ
    console.log(`行${i + 1}: 名前="${name}", 電話番号="${phoneNumber}", メール="${email}"`);

    if (name) { // 名前が入力されている行のみ
      // 電話番号の検証と正規化
      let normalizedPhoneNumber = '';
      if (phoneNumber !== null && phoneNumber !== undefined && phoneNumber !== '') {
        // 数値として読み込まれた場合の対応(先頭0を復元)
        let phoneStr = phoneNumber.toString().trim();

        // 数値で読み込まれて先頭の0が消えた場合を検出・修正
        if (/^[7-9][0-9]{9}$/.test(phoneStr)) {
          // 携帯電話番号の場合(70~99で始まる10桁)
          phoneStr = '0' + phoneStr;
          console.log(`先頭0を復元: ${phoneNumber} -> ${phoneStr}`);
        } else if (/^20[0-9]{8,11}$/.test(phoneStr)) {
          // 020番号の場合
          phoneStr = '0' + phoneStr;
          console.log(`先頭0を復元: ${phoneNumber} -> ${phoneStr}`);
        } else if (/^[1-6][0-9]{8,9}$/.test(phoneStr)) {
          // 固定電話番号の場合(01~06で始まる)
          phoneStr = '0' + phoneStr;
          console.log(`先頭0を復元: ${phoneNumber} -> ${phoneStr}`);
        }

        // ハイフンやスペースを除去
        normalizedPhoneNumber = phoneStr.replace(/[-\s]/g, '');

        // 電話番号の形式チェック
        if (!/^(0[1-9][0-9]{8,9}|0[7-9]0[0-9]{8})$/.test(normalizedPhoneNumber)) {
          console.warn(`警告: ${name}の電話番号が無効です: "${phoneNumber}" -> "${normalizedPhoneNumber}"`);
          console.warn(`有効な形式: 携帯電話(070/080/090-XXXX-XXXX)、固定電話(0X-XXXX-XXXX)`);

          // 完全に無効な場合はスキップ
          if (normalizedPhoneNumber.length < 10) {
            console.warn(`${name}をスキップします(電話番号が短すぎる)`);
            continue;
          }
        }
      } else {
        console.warn(`警告: ${name}の電話番号が未入力です`);
        continue;
      }

      const contact = {
        name: name.toString().trim(),
        phoneNumber: normalizedPhoneNumber,
        email: email ? email.toString().trim() : '',
        note: note ? note.toString().trim() : ''
      };

      contacts.push(contact);
      console.log(`✓ 有効な連絡先: ${contact.name} (${contact.phoneNumber})`);
    }
  }

  console.log(`合計${contacts.length}件の有効な連絡先を読み込みました`);
  return contacts;
}

/**
 * ログエントリをシートに書き込み
 */
function writeLogToSheet(sheet, logEntry) {
  const row = [
    logEntry.timestamp,
    logEntry.name,
    logEntry.phoneNumber,
    logEntry.email || '',
    logEntry.deliveryOrderId,
    logEntry.success ? '成功' : '失敗',
    logEntry.errorCode,
    logEntry.errorMessage
  ];

  sheet.appendRow(row);
}

/**
 * 配信結果詳細をシートに書き込み
 */
function writeDeliveryResults(sheet, type, deliveryOrders) {
  // ヘッダー設定
  if (sheet.getLastRow() === 0) {
    const headers = ['タイプ', '配信オーダーID', 'ステータス', '受付日時', '完了日時', '配信ID', '宛先', '配信ステータス', '配信日時', '配信停止希望', 'エラーコード'];
    sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
    sheet.getRange(1, 1, 1, headers.length).setFontWeight('bold');
  }

  deliveryOrders.forEach(order => {
    order.deliveries.forEach(delivery => {
      const row = [
        type,
        order.id,
        order.status,
        order.accepted_at,
        order.end_at || '',
        delivery.id,
        delivery.to,
        delivery.status,
        delivery.delivered_at || '',
        delivery.opted_out,
        delivery.error ? delivery.error.code : ''
      ];
      sheet.appendRow(row);
    });
  });
}

/**
 * メッセージテンプレートを取得
 */
function getMessageTemplate() {
  // 設定シートから読み込むか、固定メッセージを返す
  return `【重要なお知らせ】

いつもご利用いただきありがとうございます。
システムメンテナンスのお知らせです。

メンテナンス日時:${Utilities.formatDate(new Date(), 'JST', 'yyyyMMdd HH:mm')}

詳細は弊社ウェブサイトをご確認ください。
https://example.com/maintenance

配信停止を希望される場合は {{配信停止URL}} をクリックしてください。`;
}

/**
 * 結果サマリーを表示
 */
function showResultSummary(results) {
  const successCount = results.filter(r => r.success).length;
  const failureCount = results.length - successCount;

  console.log('\n=== 配信結果サマリー ===');
  console.log(`総配信数: ${results.length}`);
  console.log(`成功: ${successCount}`);
  console.log(`失敗: ${failureCount}`);

  if (failureCount > 0) {
    console.log('\n失敗詳細:');
    results.filter(r => !r.success).forEach(result => {
      console.log(`- ${result.name}: ${result.errorCode} - ${result.errorMessage}`);
    });
  }
}

/**
 * 既存の連絡先シートの電話番号フォーマットを修正
 */
function fixPhoneNumberFormat() {
  console.log('=== 電話番号フォーマット修正 ===');

  try {
    const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
    const contactsSheet = getOrCreateSheet(spreadsheet, '連絡先');

    // 電話番号列を文字列フォーマットに設定
    const phoneNumberColumn = contactsSheet.getRange('B:B');
    phoneNumberColumn.setNumberFormat('@');
    console.log('✓ 電話番号列を文字列フォーマットに設定しました');

    // 既存データの電話番号を修正
    const data = contactsSheet.getDataRange().getValues();
    let fixedCount = 0;

    for (let i = 1; i < data.length; i++) { // ヘッダー行をスキップ
      const name = data[i][0];
      const phoneNumber = data[i][1];

      if (name && phoneNumber) {
        let phoneStr = phoneNumber.toString().trim();
        let originalPhone = phoneStr;
        let fixed = false;

        // 先頭0が消えた場合の修正
        if (/^[7-9][0-9]{9}$/.test(phoneStr)) {
          phoneStr = '0' + phoneStr;
          fixed = true;
        } else if (/^20[0-9]{8,11}$/.test(phoneStr)) {
          phoneStr = '0' + phoneStr;
          fixed = true;
        } else if (/^[1-6][0-9]{8,9}$/.test(phoneStr)) {
          phoneStr = '0' + phoneStr;
          fixed = true;
        }

        if (fixed) {
          contactsSheet.getRange(i + 1, 2).setValue(phoneStr);
          console.log(`修正: ${name} の電話番号 ${originalPhone} -> ${phoneStr}`);
          fixedCount++;
        }
      }
    }

    console.log(`✓ ${fixedCount}件の電話番号を修正しました`);
    console.log('修正完了: 今後は電話番号を入力する際に先頭の0が保持されます');

  } catch (error) {
    console.error('電話番号フォーマット修正中にエラーが発生しました:', error);
  }
}

3.Google Apps Scriptの「プロジェクトの設定」→「スクリプト プロパティ」でAPIトークンを設定

スクリーンショット_2025-07-22_10_43_13.png (87.0 kB)

4.実際に送信してみる

  • 連絡先ファイルを作成する
  • sendSMSFromSpreadsheetを実行すると下記エラーが発生
API Request Error: { [Exception: Request failed for https://sandbox.cpaasnow.com returned code 400. Truncated server response: {"code":"InvalidParameter","message":"パラメータに誤りがあります","details":[{"parameter":"to","message":"toは070,080,090から始まる11桁の番号か、020から始まる11桁または14桁の番号にしてください... (use muteHttpExceptions option to examine full response)] name: 'Exception' }
  • 先頭の0が消えているのが原因なのでAIエージェントに修正依頼
連絡先シートの電話番号が文字列ではないので先頭の0が認識できない
  • fixPhoneNumberFormat関数を用意してくれたので実行する

  • sendSMSFromSpreadsheetを再度実行し送信した

  • 成功した :tada:

  • 連絡先シートの内容
    スクリーンショット 2025-07-22 13.53.01.png (46.6 kB)

  • 配信ログシートの内容(結果が反映されていきます)
    gas_cpaas.gif

感想

  • 100%AIエージェントにお任せして実装はできなかったが、適宜指摘していったら簡単に作成することができた
  • スクリプトでもコード量は多いと感じた、必要ないコードは自分で適宜メンテナンスする必要がありそう
  • スプレッドシートに登録した連絡先に対して送信するアプリはできたので、実際に利用シーンがあるといいなと思う
11
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?