moto blog

SlackとGASで作る雑談Bot ( docomo 自然対話 ( 雑談対話 ) API )

f:id:nmmmk:20180503172932j:plain

GAS(Google Apps Script)とdocomo自然対話(雑談対話)APIを使用して、SlackのChat Botを作成します。 Botに話しかけると、気の利いた言葉(面白い言葉)を返してくれて、楽しいですよ〜(^ ^)

GASを使用すると、サーバレスで気軽にBotを作ることができますのでオススメです!

以下の記事で使っていたdocomo雑談対話APIの提供が終了したため、docomo自然対話(雑談対話)APIで同じ内容のBotを作成します。
nmmmk.hatenablog.com

構造

自然対話(雑談対話)APIは旧雑談対話APIと異なり、ユーザー毎にappIdを取得することで、ユーザー毎の会話のステータス管理が行えるようになっています。

本プログラムでは、取得したappIdをFusion Tablesに保存するようにしています。

以下のような流れで動作します。
1. Slack上のBotに話しかける。
2. Slackから受信したメッセージからユーザーIDを取得する。
3. ユーザーIDを元にFusion TablesからappIdを取得する。
4. appIdが取得できなかった場合は、docomo自然対話(雑談対話)APIに対してユーザー登録を行い、appIdを取得、Fusion TablesへのappId登録を行う。
5. docomo自然対話(雑談対話)APIにメッセージを送信する。
6. docomo自然対話(雑談対話)APIから回答が返ってくる。
7. Slackへdocomo自然対話(雑談対話)APIの回答を送信する。

f:id:nmmmk:20181029210801p:plain

Slackの設定

Incoming Webhooksの設定

GASからメッセージを受け取れる様に、Incoming Webhooksの設定を行います。
以下にアクセスして設定してください。
https://api.slack.com/apps

  1. Create New Appを押下します。
    f:id:nmmmk:20180502142058p:plain

  2. 作成するBotの名称設定と対象のワークスペースを選択します。
    f:id:nmmmk:20180502112518p:plain

  3. Incoming Webhooksの設定をONにします。 f:id:nmmmk:20180502134810p:plain

  4. Add a Bot Userを押下します。
    f:id:nmmmk:20180502134836p:plain

  5. Botの名称を設定します。
    f:id:nmmmk:20180502134852p:plain

  6. アプリケーションをインストールします。
    f:id:nmmmk:20180502134918p:plain

  7. 投稿先を設定します。
    f:id:nmmmk:20180502135027p:plain

  8. Botのアイコンを設定します(任意) f:id:nmmmk:20180502135354p:plain

  9. Incoming WebhooksのURLを確認します。
    GASの実装でこのURLを使用します。
    f:id:nmmmk:20180502135558p:plain

Outgoing Webhooksの設定

GASへメッセージを送信できる様に、Outgoing Webhooksの設定を行います。

  1. 以下にアクセスして、発信Webフックの設定を追加します。
    https://slack.com/apps/A0F7VRG6Q-outgoing-webhooks
    f:id:nmmmk:20181025230310p:plain

  2. 発信Webフック インテグレーションの追加を押下します。
    f:id:nmmmk:20181025230408p:plain

  3. インテグレーションの設定部分までスクロールします。

  4. 引き金となる言葉には、メッセージの送信相手+「:」と考えてください。
    例えば、以下の様な投稿の仕方となります。
    例)ZatsudanBot: テストメッセージ

  5. URLの部分は、後述するGASのURLを設定します。

  6. トークンの部分はGASで使用します。
    f:id:nmmmk:20180502140131p:plain

docomo自然対話(雑談対話)APIの登録

今回使用するのは、docomo自然対話(雑談対話)APIです。
dev.smt.docomo.ne.jp

必要事項を入力して、APIを申請し、APIキーを取得します。
f:id:nmmmk:20180502104656p:plain

appIdの保管場所として、Fusion Tablesを作成する

カラムとして、UserId: Text, AppId: Textのテーブルを作成します。

f:id:nmmmk:20181029221015p:plain

Fusion Tablesの使用方法は、以下の記事を参考にしてください。
nmmmk.hatenablog.com

GASのソースコードとして、定数関連を定義する

定数 内容
WWWWW Outgoing Webhooks設定時に割り当てられたトークンを使用
XXXXX Incoming Webhooksの設定時に取得した投稿先のチャンネルのURLを使用
YYYYY Fusion TablesのIDをを使用
ZZZZZ 自然対話(雑談対話)API KEYを使用
// Outgoing webhooksのToken
var SlackOutgoingWebhooksToken = "WWWWW";

// Slackの投稿先チャンネル
var SlackPostChannel = "XXXXX";

// FusionTableのID
var FusionTableId = "YYYYY";

// Docomo APIのキー
var DocomoApiKey = "ZZZZZ";

GASでSlackからのメッセージを受け取れる様にする

基本となるソースコード

doPost()でSlackからのメッセージを受け取ります。
また、addLog()で受信した情報をスプレッドシートに書き込みます。

AAAAAの部分はスプレッドシートのID、BBBBBの部分はシート名を指定してください。

スプレッドシートのIDは、スプレッドシートのURLの下記の部分です。
https://docs.google.com/spreadsheets/d/スプレッドシートのID/edit#gid=0

//-----------------------------
// Slackからのメッセージ受信
//-----------------------------
function doPost(e) {

  // SlackのOutgoing WebhooksのTokenであるかを比較
  if (e.parameter.token != SlackOutgoingWebhooksToken) {
    return;
  }
  
  // テキスト部を抜き出し
  var text = e.parameter.text;

  // 「:」でテキスト分割する
  text = text.split(":");
  
  if (text[1].length > 1) {
    
    addLog(text[1]);
  }
}

//-----------------------------
// スプレッドシートへのログ出力
//-----------------------------
function addLog(text) {
  var spreadsheetId = "AAAAA";
  var sheetName = "BBBBB";
  var spreadsheet = SpreadsheetApp.openById(spreadsheetId);
  var sheet = spreadsheet.getSheetByName(sheetName);
  sheet.appendRow([new Date(),text]);
  return text;
}

GASをウェブアプリケーションとして公開する

  1. スクリプトエディタにて公開 -> ウェブアプリケーションとして導入 を選択します。 f:id:nmmmk:20180502115443p:plain

  2. 公開設定を行います。 以下の様に設定します。
    プロジェクトバージョンは、新規作成
    アプリケーションにアクセスできるユーザーは、全員(匿名ユーザーを含む)
    f:id:nmmmk:20180502152239p:plain

  3. 割り当てられたURLをSlackのOutgoing WebhooksのURLに設定します。 f:id:nmmmk:20180502152225p:plain

ソースコードを修正した場合は、1, 2の手順を毎回行う必要がありますので注意してください。

Slackからメッセージを送信する

以下の様にメッセージを送信します。
ZatsudanBot: テスト

f:id:nmmmk:20180502222544p:plain

スプレッドシートを確認すると、「テスト」が記録されていますのでGASでのメッセージ受信ができていることが分かります。
f:id:nmmmk:20180502222310p:plain

メンションの変換文字列を取得する

現時点では、メンションでの呼び出しに対応していません。
@ZatsudanBot テスト と投稿しても反応しません。
仮に、Outgoing Webhooksの「引き金となる言葉」を「@ZatsudanBot」に変更しても、期待した動作とはなりません。

「@ZatsudanBot」が「 <@UAG876W49>」の様なユーザーID文字列に変換されているためです。

ユーザーID文字列を取得するために、以下のメッセージを送信します。
ZatsudanBot: @ZatsudanBot
f:id:nmmmk:20180502220727p:plain

スプレッドシートを参照すると、「 <@UAG876W49>」の記載があることが分かります。
これがユーザーIDです。

f:id:nmmmk:20180502220740p:plain

取得したIDをOutgoing Webhooksの「引き金となる言葉」に設定します。

f:id:nmmmk:20180502221127p:plain

Slackへの投稿処理を準備する

function postSlack(text) {
  // Incoming WebhooksのURL
  var url = SlackPostChannel;
  
  var payload = {
    text: text
  };
 
  var options = {
    "method" : "POST",
    "headers": {"Content-type": "application/json"},
    "payload": JSON.stringify(payload)
  };
  UrlFetchApp.fetch(url, options); 
}

docomo自然対話(雑談対話)APIとの送受信処理を作成する

APIの仕様は、公式 のガイドラインをご確認ください。

ユーザー登録処理

function registUser() {
  
  var regist_options = {
    'botId': 'Chatting',
    'appKind': 'Smart Phone'
  }
  
  var options = {
    'method': 'POST',
    'contentType': 'application/json',
    'payload': JSON.stringify(regist_options)
  };

  // APIのURL
  var registUrl = "https://api.apigw.smt.docomo.ne.jp/naturalChatting/v1/registration?APIKEY=" + DocomoApiKey;
  
  // docomo APIと通信する
  var response = UrlFetchApp.fetch(registUrl, options);

  // docomo APIからの回答はJSON形式なのでオブジェクト変換
  var content = JSON.parse(response.getContentText());

  // docomo APIから取得したappIdを返す
  return content.appId; 
}

雑談対話処理

function getDialogueMessage(appId, mes, recvTime) {

  // 送信時刻  
  var sendTime = dateToString(new Date());
  
  var dialogue_options = {
    'language': 'ja-JP',
    'botId': 'Chatting',
    'appId': appId,
    'voiceText': mes,
    'appRecvTime': recvTime,
    'appSendTime': sendTime,
  }
  var options = {
    'method': 'POST',
    'contentType': 'application/json',
    'payload': JSON.stringify(dialogue_options)
  };

  // APIのURL
  var dialogueUrl = "https://api.apigw.smt.docomo.ne.jp/naturalChatting/v1/dialogue?APIKEY=" + DocomoApiKey;
  
  // docomo APIと通信する
  var response = UrlFetchApp.fetch(dialogueUrl, options);

  // docomo APIからの回答はJSON形式なのでオブジェクト変換
  var content = JSON.parse(response.getContentText());

  // docomo APIから取得した回答部分を呼び出し元に戻す
  return content.systemText.expression;
}

function dateToString(date) {
  return Utilities.formatDate(date, "JST", "yyyy-MM-dd hh:mm:ss");
}

Fusion TablesへのappIdの登録・取得処理を作成する

appId登録

function insertAppIdByUser(user, appId) {
  var tableId = FusionTableId;
  var sql = 'INSERT INTO ' + tableId + '(UserId, AppId) VALUES (' + '\'' + user + '\'' + ',\'' + appId + '\'' + ')';
  var res = FusionTables.Query.sql(sql);
  
  return res;
}

appId取得

function getAppIdFromUser(user) {
  var tableId = FusionTableId;
  var sql = 'SELECT AppId FROM ' + tableId + ' WHERE UserId=' + '\'' + user + '\'';
  var res = FusionTables.Query.sql(sql);
  
  var appId = '';
  if (res.rows) {
    appId = res.rows[0][0];
  }
 
  return appId;
}

メンション対応、Fusion Tables制御、docomo API制御を追加する

doPost()を以下の様に変更します。
※addLog()は削除しましたが、必要に応じて実装してください。

function doPost(e) {
  
  // Slackのoutgoing-webhooksのTokenであるかを比較
  if (e.parameter.token != SlackOutgoingWebhooksToken) {
    return;
  }

  // 受信時刻
  var recvTime = dateToString(new Date());
  
  // テキスト部を抜き出し
  var text = e.parameter.text;

  // <@UCV7J74BF>の部分は削除
  text = text.substring(13);

  if (text.length > 1) {

    var userId = e.parameter.user_id;
    var appId = getAppIdFromUser(userId);
     
    if (appId === '') {
      // docomo APIと通信し、ユーザー登録(appId取得)を行う
      appId = registUser();
      
      // Fusion TableにappIdを登録
      insertAppIdByUser(userId, appId);
    }

    // docomo APIへメッセージを送信し、回答を受信する
    var dialogueMessage = getDialogueMessage(appId, text, recvTime);
    
    // 返信メッセージ作成
    var message = "<@" + userId + "> " + dialogueMessage;
    
    // Slackへ送信
    postSlack(message);
  }
}

最終ソースコード

全体

// Outgoing webhooksのToken
var SlackOutgoingWebhooksToken = "WWWWW";

// Slackの投稿先チャンネル
var SlackPostChannel = "XXXXX";

// FusionTableのID
var FusionTableId = "YYYYY";

// Docomo APIのキー
var DocomoApiKey = "ZZZZZ";

//----------------------------------------------------------
// Slackからのメッセージ受信
//----------------------------------------------------------
function doPost(e) {
  
  // Slackのoutgoing-webhooksのTokenであるかを比較
  if (e.parameter.token != SlackOutgoingWebhooksToken) {
    return;
  }

  // 受信時刻
  var recvTime = dateToString(new Date());
  
  // テキスト部を抜き出し
  var text = e.parameter.text;

  // <@UCV7J74BF>の部分は削除
  text = text.substring(13);

  if (text.length > 1) {

    var userId = e.parameter.user_id;
    var appId = getAppIdFromUser(userId);
     
    if (appId === '') {
      // docomo APIと通信し、ユーザー登録(appId取得)を行う
      appId = registUser();
      
      // Fusion TableにappIdを登録
      insertAppIdByUser(userId, appId);
    }

    // docomo APIへメッセージを送信し、回答を受信する
    var dialogueMessage = getDialogueMessage(appId, text, recvTime);
    
    // 返信メッセージ作成
    var message = "<@" + userId + "> " + dialogueMessage;
    
    // Slackへ送信
    postSlack(message);
  }
}

//----------------------------------------------------------
// Fusion TableからappIdを取得
//----------------------------------------------------------
function getAppIdFromUser(user) {
  var tableId = FusionTableId;
  var sql = 'SELECT AppId FROM ' + tableId + ' WHERE UserId=' + '\'' + user + '\'';
  var res = FusionTables.Query.sql(sql);
  
  var appId = '';
  if (res.rows) {
    appId = res.rows[0][0];
  }
 
  return appId;
}

//----------------------------------------------------------
// Fusion TableにappIdを登録
//----------------------------------------------------------
function insertAppIdByUser(user, appId) {
  var tableId = FusionTableId;
  var sql = 'INSERT INTO ' + tableId + '(UserId, AppId) VALUES (' + '\'' + user + '\'' + ',\'' + appId + '\'' + ')';
  var res = FusionTables.Query.sql(sql);
  
  return res;
}

//----------------------------------------------------------
// Slackへ投稿
//----------------------------------------------------------
function postSlack(text) {
 
  // 投稿先チャンネル
  var url = SlackPostChannel;
  
  var payload = {
    "text": text
  };
 
  var options = {
    "method" : "POST",
    "headers": {"Content-type": "application/json"},
    "payload": JSON.stringify(payload)
  };
  
 
  UrlFetchApp.fetch(url, options); 
}

//----------------------------------------------------------
// docomo APIと通信し、ユーザー登録(appId取得)を行う
//----------------------------------------------------------
function registUser() {
  
  var regist_options = {
    'botId': 'Chatting',
    'appKind': 'Smart Phone'
  }
  
  var options = {
    'method': 'POST',
    'contentType': 'application/json',
    'payload': JSON.stringify(regist_options)
  };

  // APIのURL
  var registUrl = "https://api.apigw.smt.docomo.ne.jp/naturalChatting/v1/registration?APIKEY=" + DocomoApiKey;
  
  // docomo APIと通信する
  var response = UrlFetchApp.fetch(registUrl, options);

  // docomo APIからの回答はJSON形式なのでオブジェクト変換
  var content = JSON.parse(response.getContentText());

  // docomo APIから取得したappIdを返す
  return content.appId; 
}

//----------------------------------------------------------
// docomo APIへメッセージを送信し、回答を受信する
//----------------------------------------------------------
function getDialogueMessage(appId, mes, recvTime) {

  // 送信時刻  
  var sendTime = dateToString(new Date());
  
  var dialogue_options = {
    'language': 'ja-JP',
    'botId': 'Chatting',
    'appId': appId,
    'voiceText': mes,
    'appRecvTime': recvTime,
    'appSendTime': sendTime,
  }
  var options = {
    'method': 'POST',
    'contentType': 'application/json',
    'payload': JSON.stringify(dialogue_options)
  };

  // APIのURL
  var dialogueUrl = "https://api.apigw.smt.docomo.ne.jp/naturalChatting/v1/dialogue?APIKEY=" + DocomoApiKey;
  
  // docomo APIと通信する
  var response = UrlFetchApp.fetch(dialogueUrl, options);

  // docomo APIからの回答はJSON形式なのでオブジェクト変換
  var content = JSON.parse(response.getContentText());

  // docomo APIから取得した回答部分を呼び出し元に戻す
  return content.systemText.expression;
}

//----------------------------------------------------------
// date -> 文字列変換(yyyy-MM-dd hh:mm:ss形式文字列で返す)
//----------------------------------------------------------
function dateToString(date) {
  return Utilities.formatDate(date, "JST", "yyyy-MM-dd hh:mm:ss");
}

実行してみる!

f:id:nmmmk:20180502223956p:plain

ごもっともです・・・