Slack上にラブライブの絢瀬絵里を召還したらポンコツだった件

こんばんは!エンジニアのTです。

つい先日、入社した期待の若手新人S君が、このトンガルマンブログ内の、僕の記事を見てくれていたのがなんだか嬉しかったです。

その時は、艦隊これくしょんというゲームをやっています〜的な所から出だしていましたが、その時は2013年10月の更新でした。未だにやり続けている上に、提督レベルは104まで上がり、ケッコン艦(Lv100〜)は19隻となりました。まだまだ、やっていきますよ〜!

「秋津洲」 秋津洲型 1番艦 水上機母艦

秋津洲(あきつしま)、大和国の異称。日本を指す言葉らしいです。まめ知識!

Slack

さて、最近社内ではSlackを試験的に導入しました(強引に)

Slackとは、チームコミュニケーションツールです。さくっと言えば、チャット上にいろんな情報を流して共有できたり、アカウント管理を中央集権的に行ったりと、Skype等と比べると、いろいろな事が出来るようになります。

Slackの最大の目玉(というかこれ目的)は、何と言ってもインテグレーション機能でしょう。これで様々なサービス(例えば、GitHubやJenkins等)と連携を行う事ができます!

Botを作る

今回は、このインテグレーション機能を使って、Botを作ってみました。
巷ではHubotというGitHub謹製のBot開発・実行フレームワークでのサンプルが多いです。そちらを試したかったのですが、いろいろな事情により断念。

なので、Google Apps Scriptを使って実装する事にしました。
ただ、普通のBotだと、特定のメッセージに対して、特定の応答しかしないので面白みに欠けます。

じゃあ、どうするか・・・



そうだ!会話させよう!!!



という所から、ドコモが提供する雑談対話APIを利用する事にしました。


ついでに、Slackへ通知した時の名前とアイコンをラブライブの絢瀬絵里にしたらテンションあがんじゃね?



それでは早速・・・

用意する物

  • 雑談対話APIの実行環境(ドコモデベロッパーへの登録とAPIキーの発行)
  • Google Apps Scriptの実行環境(Googleアカウントがあればドライブから作れます)
  • SlackのIncoming WebHooksを有効化
  • SlackのOutgoing WebHooksを有効化
この辺は、参考になるページがいっぱいありますので、適当に検索してみてください。僕もお世話になりました。

まずは、Slack上へ任意のメッセージを流せるようにライブラリを作成します。
var SLACK_URL = "https://hooks.slack.com/services/******/***/******";

/**
 * 指定のURLからJSONデータを取得します。
 */ 
function commonGetJsonFrom(url, options) {
  var data = commonGetDataFrom(url, options);
  
  if (data) {
    return JSON.parse(data);
  }
  
  return null;
}

/**
 * Slackへ送信する為のJSONの作成
 */ 
function slackMakeResponse(payload) {
  var headers = {"Accept":"application/json", 
                "Content-Type":"application/json", 
                   "Authorization":"Basic _authcode_"
               };
  var options = { "method" : "POST",
                  "contentType" : "application/json",
                  "headers" : headers,
                  "payload" : payload
              };
  
  return options;
}

/**
 * スラックへメッセージを通知する
 */
function slackSend(response) {
  var response = UrlFetchApp.fetch(SLACK_URL, response);  
}

/**
 * ペイロードの作成
 */
function slackMakePayload(channel, username, emoji, text) {
  var data = {"channel": channel, "username": username, "text": text, "icon_emoji": emoji};
  return JSON.stringify(data);
}

var SlackRequest = (function() {
  var SlackRequest = function(parameter) {
    this.user_name    = parameter.user_name;
    this.channel_id   = parameter.channel_id;
    this.channel_name = parameter.channel_name;
    this.trigger_word = parameter.trigger_word;
    this.token        = parameter.token;
    this.team_id      = parameter.team_id;
    this.team_domain  = parameter.team_domain;
    this.timestamp    = parameter.timestamp;
    this.user_id      = parameter.user_id;
    this.text         = parameter.text;
  };
  
  var p = SlackRequest.prototype;
  
  /**
   * BOTの発言かどうかを返します。
   */
  p.isSlackBot = function() {
    if (this.user_name == 'slackbot') {
      return true;
    }
    
    return false;
  }
  
  /**
   * トリガーワードを除いたテキストを返します。
   */
  p.getTextWithoutTrigger = function() {
    if (this.trigger_word) {
      return this.text.substr(this.trigger_word.length);
    } else {
      return this.text;
    }
  }
  
  return SlackRequest;
})();

次に、Slackから受け取ったメッセージを処理するスクリプトを作成
var SLACK_TOKEN      = '***************';
var SLACK_TOKEN_TEST = '***************';
var SHEET_ID         = '***************';
var SS               = SpreadsheetApp.openById(SHEET_ID);
var DATA             = SS.getDataRange().getValues();

function doPost(e) {
  var payload;
  
  // パラメーターがあるか
  if (!e && !e.parameter) return null;
  
  var request = new Utils.SlackRequest(e.parameter);
  
  // botからでないか確認
  if (request.isSlackBot()) {
    return null;
  }
  
  // トークンの正当性確認
  if (request.token != SLACK_TOKEN && request.token != SLACK_TOKEN_TEST) {
    throw new Error("invalid token.");
  }
  
  var channel   = '#' + request.channel_name;
  var text      = request.getTextWithoutTrigger();
  
  var length    = DATA.length;
  var foundText = '';
  var username  = 'えりちー';
  var emoji     = ':eri:';
  var foundIndex = -1;
  
  // 発言に合致する返しを検索
  for (var i = 1; i < length; i++) {
    var regExp = new RegExp(DATA[i][0], 'i');
    if (text.match(regExp)) {
      foundText = DATA[i][1];
      username = DATA[i][2];
      emoji = DATA[i][3];
      foundIndex = i;
      break;
    }
  }
    
  if (foundText != '') {
    // 雑談
    if (foundText == 'talk') {
      var regExp = new RegExp(DATA[foundIndex][0], 'i');
      var match = text.match(regExp);
      var result = talk(request.channel_name, request.user_name, match[2], DATA[foundIndex][4]);
      
      if (result) {
        foundText = result.utt;
      } else {
        foundText = 'その発言は理解できないわ';
      }
    }
    
    if (channel == '') channel = '#in_test';
    
    // 通知
    var payload  = Utils.slackMakePayload(channel, username, emoji, foundText);
    var response = Utils.slackMakeResponse(payload);
    Utils.slackSend(response);
  }
  
  return null;
}

受け取ったメッセージを雑談対話APIへ流して、その結果を取得。
var DOCOMO_TOKEN    = '**********';
var DOCOMO_TALK_URL = 'https://api.apigw.smt.docomo.ne.jp/dialogue/v1/dialogue?APIKEY=';

function talk(channel, name, comment, characterName) {
  var properties = PropertiesService.getScriptProperties();
  var context = '';
  var channelKey = 'channel_' + channel + '_' + characterName;
  var lastTalkDateKey = channelKey + '_lastTalkDate';
  var lastTalkDate = new Date(properties.getProperty(lastTalkDateKey));
  var now = new Date();
  
  if (!lastTalkDate) {
    lastTalkDate = new Date(0);
  }
  
  // チャンネルのコンテキストIDが保存されてるか
  if (channel) {
    var channelConfigProp = properties.getProperty(channelKey);
    var channelConfig;
    
    if (!channelConfigProp) {
      channelConfig = {
        'name': channel,
        'context': '',
      };
    } else {
      channelConfig = JSON.parse(channelConfigProp);
    }
  }
  
  // 前回の会話から10分空いたら会話を新規にする
  if (now.getTime() - lastTalkDate.getTime() > 600000) {
    channelConfig.context = '';
  }
  
  var payload = {
    "utt":comment,
    "nickname": name,
    "mode": "dialog",
    "place": "大阪",
  };
  
  if (characterName == 'non') {
    payload.t = '20';
  }
  
  if (channelConfig) {
    payload.context = channelConfig.context;
  }
  
  var options = {
      "method"  : "POST",
      "contentType": "application/json",
      "payload" : JSON.stringify(payload),
  };
  
  // 会話の取得
  var result = Utils.commonGetJsonFrom(DOCOMO_TALK_URL + DOCOMO_TOKEN, options);
  
  if (channelConfig && result) {
    if (result.context != channelConfig.context) {
      // コンテキストを保存
      channelConfig.context = result.context;
      properties.setProperty(channelKey, JSON.stringify(channelConfig));
      properties.setProperty(lastTalkDateKey, Utilities.formatDate(now, 'Asia/Tokyo', 'yyyy/MM/dd HH:mm:ss'));
    }
  }
  
  return result;
}


結果・・・!



ポンコツぅぅぅ!!!!!!!


いや、これはこれで、かわいい・・・

これでいつでも絢瀬絵里を召還できるようになりました。



GAS側の処理概要なのですが、主に下記のようになっています。
  • Slackから送られてきたメッセージの先頭が「えりちー」の場合、雑談対話APIへ流す
  • 最後の会話から10分以上経過していた場合は、会話のコンテキストを破棄して、新しい会話を開始する。
  • 特定のチャンネルの全ての会話を拾うようになっているので、Botからのメッセージの場合はスルーするようにした(永遠にBot同士の会話が続いたりするので)。nicknameで判断します。
また、受け取ったメッセージなのですが、別途スプレッドシートを用意して、正規表現で内容を振り分けるようにしています。

後、よくわからなかったのが、Outgoing WebHooksを有効にしている場合、GAS側のdoPostで受け取った後、レスポンスを返せばそのままSlackへ通知できるらしいのですが、どんな内容を返してもSlackへ通知できませんでした・・・何かが間違っているのでしょうけど、原因はまだ見つけられていません。なので、Incoming WebHooks側のトークンを使ってSlackへ通知を行っています。

本当は、他にもいろいろな機能を実装しているのですが(画像検索、天気予報、Googleカレンダーの内容を表示、地震速報etc)とりあえず、会話部分以外を削除して掲載しています。



あなたの職場にもお好みのキャラを光臨させて、一時の静養としてみてはいかがでしょうか?

また、何か面白い使い方を見つけたらネタにさせて頂きます!


トンガルマンWebサイト
https://tongullman.co.jp/index.php
facebook
https://www.facebook.com/Tongullman

0 件のコメント:

コメントを投稿