コンテンツにスキップ

Slackに架電結果を通知する

nocall の通話完了 Webhook を Google Apps Script(GAS)で受け取り、Slack の Incoming Webhook へ架電結果を通知します。

このサンプルが Slack へ送信する項目は、エージェント名、通話結果、電話番号の末尾 4 桁、通話時間、nocall の通話詳細画面へのリンクです。会話本文、コンタクト属性、通話後分析は Slack へ送信しません。

  • Google アカウントと nocall アカウントがある
  • nocall で対象エージェントの Webhook 設定を変更できる
  • Slack アプリを作成し、通知先チャンネルへ追加できる

1. SlackのIncoming Webhookを作成する

Section titled “1. SlackのIncoming Webhookを作成する”
  1. Slack API のアプリ管理画面で「Create New App」を選択します。
  2. 「From scratch」を選択し、アプリ名と通知先のワークスペースを設定します。
  3. 「Incoming Webhooks」を開き、「Activate Incoming Webhooks」を有効にします。
  4. 「Add New Webhook to Workspace」を選択します。
  5. 通知先チャンネルを選択して許可します。
  6. 作成された Webhook URL をコピーします。

Webhook URL は、その URL を知っている人が Slack へ通知を投稿できる秘密情報です。チャット、チケット、ソースコードへ貼り付けないでください。

  1. Google Apps Scriptで新しいプロジェクトを作成します。
  2. プロジェクト名を「nocall Slack 通知」など、用途が分かる名前へ変更します。
  3. Apps Script の「プロジェクトの設定」→「スクリプト プロパティ」に次の値を追加します。
プロパティ
SLACK_WEBHOOK_URL 手順 1 でコピーした Slack の Webhook URL
NOCALL_WEBHOOK_SECRET 英数字、-_ だけで生成した 32 文字以上のランダムな文字列

NOCALL_WEBHOOK_SECRET はパスワード管理ツールなどで生成し、記号を含める場合も -_ だけを使用してください。

Apps Script の Code.gs を開き、元の内容をすべて削除してから、次のコードをそのまま貼り付けて保存します。

const SLACK_WEBHOOK_URL_PROPERTY = "SLACK_WEBHOOK_URL";
const NOCALL_WEBHOOK_SECRET_PROPERTY = "NOCALL_WEBHOOK_SECRET";
const MAX_SLACK_MESSAGE_LENGTH = 2900;
function doPost(event) {
const expectedSecret = getRequiredProperty(NOCALL_WEBHOOK_SECRET_PROPERTY);
const actualSecret = String(event.parameter?.key ?? "");
if (!constantTimeEqual(actualSecret, expectedSecret)) {
throw new Error("Webhook secretが一致しません");
}
if (!event.postData?.contents) {
throw new Error("Webhook payloadが空です");
}
const call = JSON.parse(event.postData.contents);
const response = UrlFetchApp.fetch(
getRequiredProperty(SLACK_WEBHOOK_URL_PROPERTY),
{
method: "post",
contentType: "application/json",
payload: JSON.stringify(buildSlackMessage(call)),
muteHttpExceptions: true,
},
);
const responseCode = response.getResponseCode();
if (responseCode < 200 || responseCode >= 300) {
const responseText = truncate(response.getContentText(), 200);
throw new Error(`Slack通知に失敗しました: HTTP ${responseCode} ${responseText}`);
}
return ContentService.createTextOutput("ok");
}
function buildSlackMessage(call) {
const agentName = escapeSlack(call.agent?.name ?? "不明");
const status = escapeSlack(formatCallStatus(call.callStatus));
const lines = [
"*nocall 架電結果*",
`・エージェント: ${agentName}`,
`・ステータス: ${status}`,
`・通話先: ${escapeSlack(maskPhoneNumber(call.to))}`,
`・通話時間: ${escapeSlack(formatDuration(call.duration))}`,
];
if (call.isVoicemail === true) {
lines.push("・留守番電話: はい");
}
if (isNocallDetailsUrl(call.detailsUrl)) {
lines.push("", `<${call.detailsUrl}|nocallで通話詳細を開く>`);
}
return { text: truncate(lines.join("\n"), MAX_SLACK_MESSAGE_LENGTH) };
}
function formatCallStatus(status) {
const labels = {
preparing: "準備中",
ringing: "呼び出し中",
in_progress: "通話中",
completed: "完了",
no_answer: "応答なし",
failed: "失敗",
unreachable: "到達不能",
};
return labels[status] ?? String(status ?? "不明");
}
function formatDuration(duration) {
if (!Number.isFinite(duration) || duration < 0) return "未取得";
const minutes = Math.floor(duration / 60);
const seconds = String(Math.floor(duration % 60)).padStart(2, "0");
return `${minutes}${seconds}`;
}
function maskPhoneNumber(value) {
const digits = String(value ?? "").replace(/\D/g, "");
if (digits.length < 4) return "未取得";
return `****${digits.slice(-4)}`;
}
function isNocallDetailsUrl(value) {
return /^https:\/\/console\.nocall\.ai\//.test(String(value ?? ""));
}
function escapeSlack(value) {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
function truncate(value, maxLength) {
const text = String(value);
return text.length <= maxLength ? text : `${text.slice(0, maxLength - 1)}`;
}
function getRequiredProperty(name) {
const value = PropertiesService.getScriptProperties().getProperty(name);
if (!value) throw new Error(`${name}が設定されていません`);
return value;
}
function constantTimeEqual(leftValue, rightValue) {
const left = String(leftValue);
const right = String(rightValue);
const length = Math.max(left.length, right.length);
let difference = left.length ^ right.length;
for (let index = 0; index < length; index += 1) {
difference |= (left.charCodeAt(index) || 0) ^ (right.charCodeAt(index) || 0);
}
return difference === 0;
}

4. Webアプリとしてデプロイする

Section titled “4. Webアプリとしてデプロイする”
  1. Apps Script 右上の「デプロイ」→「新しいデプロイ」を選択します。
  2. 種類に「ウェブアプリ」を選択します。
  3. 「次のユーザーとして実行」は自分を選択します。
  4. 「アクセスできるユーザー」は、外部システムからアクセス可能な「全員」を選択します。
  5. 「デプロイ」を選択し、権限の確認を完了します。
  6. /exec で終わるウェブアプリ URL をコピーします。
  1. nocall で対象のエージェントを開きます。
  2. 「Webhook設定」を開き、「Webhookを追加」を選択します。
  3. Webhook 名に「Slack 通話結果通知」など、用途が分かる名前を入力します。
  4. URL に、GAS のウェブアプリ URL と NOCALL_WEBHOOK_SECRET を次の形式で入力します。
https://script.google.com/macros/s/DEPLOYMENT_ID/exec?key=NOCALL_WEBHOOK_SECRETの値
  1. 保存します。

この URL は共有シークレットを含むため、チャットやチケットへ貼り付けないでください。

  1. nocall の Webhook 設定で「テスト」を選択します。
  2. 「Webhookの送信に成功しました」と表示されることを確認します。
  3. Slack の通知先チャンネルにテスト用の架電結果が表示されることを確認します。
  4. Apps Script の「実行数」で doPost が「完了」になっていることを確認します。

テスト送信は固定のダミーデータを使用し、実際の電話は発信しません。

nocall の対象エージェントで Webhook 設定を開き、「Slack 通話結果通知」の Webhook を削除します。削除後に完了した通話は、この連携から Slack へ通知されません。

連携自体を廃止する場合は、Apps Script のデプロイとスクリプト プロパティを削除し、Slack アプリの設定から Incoming Webhook を削除してください。すでに Slack へ投稿されたメッセージは Incoming Webhook から削除できません。

  • nocall のテストが失敗する
    • GAS の URL が /exec で終わっているか確認する
    • URL の keyNOCALL_WEBHOOK_SECRET が完全に一致しているか確認する
    • Apps Script の「実行数」でエラー内容を確認する
  • nocall のテストは成功するが Slack に表示されない
    • SLACK_WEBHOOK_URL が正しいか確認する
    • Slack アプリが通知先チャンネルへ追加されているか確認する
    • 通知先チャンネルがアーカイブされていないか確認する
  • Slack通知に失敗しました: HTTP 429 と表示される
    • Slack への通知が集中しているため、時間を空けてテストする
    • 正式な架電結果は nocall の通話履歴で確認する

Webhook URL や共有シークレットが漏えいした可能性がある場合は、Slack の Incoming Webhook と NOCALL_WEBHOOK_SECRET の両方を新しい値へ変更してください。