5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

GoogleChatAPIを使って問い合わせを転送するBotを作ったよ

Last updated at Posted at 2020-08-12

やあ!こんばんは。:relaxed:だよ。
今日も暑かったね。お水飲んで生きていこうね。

さて今日はこれまで細切りにご報告してきたBotの総まとめをするよ。
忙しい人はこれだけ読んでね。 お時間ある人は個別記事も読んでもらえると嬉しいな。

##BOTの概要

【このBOTができること】
1.ユーザーがBOTにDMを送ったら、:relaxed:の管理するチャットルームへ転送される。
2.チャットルーム内でのユーザーごとのthread名をスプレッドシートに保持し、同じユーザーからのDMは同じスレッドに書き込まれる。
3.スレッドに:relaxed:(管理者)から書き込みをしたら、対象のユーザーにDMを送ってくれる。

【イメージ】
https---qiita-image-store.s3.ap-northeast-1.amazonaws.com-0-482103-5e0b3058-b2b0-4b98-8bc3-5774bca848c0.png

##使用したもの

実装環境
GoogleAppsScript
ライブラリ
・OAuth2
API
・Hangouts Chat API
・Google Sheets API
・Google Drive API

##PGM

BOTのEVENT部分


/**
 * Responds to a MESSAGE event in Hangouts Chat.
 *
 * @param {Object} event the event object from Hangouts Chat
 */
function onMessage(event) {
  
  const spaceName         = PropertiesService.getScriptProperties().getProperty("SPACE_NAME");
  const spreadsheetId     = PropertiesService.getScriptProperties().getProperty("SPREADSHEET_ID");
  const folderId          =  PropertiesService.getScriptProperties().getProperty("FOLDER_ID");
  const threadRangeName   = 'threadInfo!A2:E';
  const manageRangeName   = 'manageUsers!A2:E';
  
 
  var userName       = event.user.name;
  var userSpaceName  = event.space.name;
  var userThreadName = event.message.thread.name;

  var sender = "";
  var avatar = "";
  var thread = "";
  
  console.log(event);
  
  // メッセージの送信者の管理者情報を取得する。
  var manageUser  = getManageUserInfo(spreadsheetId, manageRangeName, userName, userSpaceName);
  
  // 送信者=管理者の場合、ユーザーにDMを送る。
  if(manageUser){
     
    sender = manageUser[2];
    avatar = manageUser[3];
    thread = getTheadInfo(spreadsheetId, threadRangeName, userName,spaceName, userThreadName);
    sendMessage(sender, avatar, event.message, thread[2], "", folderId);
 
  } else {
    // 送信者 != 管理者の場合、管理者のチャットルームにメッセージを転送する。
    
    sender = event.user.displayName;
    avatar = event.user.avatarUrl;
    thread = getTheadInfo(spreadsheetId, threadRangeName, userName, spaceName, "");
    
    
    if (thread){
      //過去にメッセージを送ったことがあるユーザーの場合は、同じスレッドにメッセージを送る。
      sendMessage(sender, avatar, event.message, spaceName, thread[4], folderId);
      
    } else {
     //初回のメッセージ送信の場合は、メッセージを送った後に threadIdを取得して登録する。
      var rs = JSON.parse(sendMessage(sender, avatar, event.message, spaceName, "", folderId)); 
      var values = [
        [userName, event.user.displayName, userSpaceName, spaceName ,rs.thread.name]
      ];
      appendValuesSpreadsheet(spreadsheetId, "threadInfo", values );
      
    }
  }
}


/**
 * Responds to an ADDED_TO_SPACE event in Hangouts Chat.
 *
 * @param {Object} event the event object from Hangouts Chat
 */
function onAddToSpace(event) {
  var message = "";
  message = event.user.displayName + "さん、問い合わせ窓口にようこそ! 質問・相談を書くと、担当者に届きます。";
  
  if (event.message) {
    // Bot added through @mention.
    message = message + "こちらのメッセージはすでに送られました。(" + event.message.text + "";
  }

  return { "text": message };
}

/**
 * Responds to a REMOVED_FROM_SPACE event in Hangouts Chat.
 *
 * @param {Object} event the event object from Hangouts Chat
 */
function onRemoveFromSpace(event) {
  console.info("Bot removed from ", event.space.name);
}


独自の関数


function sendMessage(sender,avatar, message, spaceId, threadId, folderId) {

  var service = getOAuth2Service(); 
  var date    = new Date();
  var widgets = [];
  
  var header  =  {
    "title"     : "From:" + sender,
    "imageUrl"  :  avatar,
    "imageStyle": "AVATAR" 
  };
  
  var textParagraph = {"text" : message.argumentText};
  
  if(message.attachment){
  
    var url = uploadAttachmentToDrive(message.attachment, folderId, Utilities.formatDate( date, 'Asia/Tokyo', 'yyyy-MM-dd_hh:mm:ss')+"_" + sender);
    var buttons =  
      { "buttons": [
        { "textButton": 
          {  "text": "添付を見る",
             "onClick": { "openLink": { "url": url}}
          }
        }
      ]};
    widgets.push(buttons);
    
  } else {
   console.log("no attachemnt");
  }
  
  widgets.push({"textParagraph":textParagraph });

  var message = {"cards":{"header": header, "sections": {"widgets":widgets}}, "thread":{ name: threadId}};
  
  var url = "https://chat.googleapis.com/v1/"+spaceId +"/messages/";
  options = {
   "method"  : "Post",
   "payload" :JSON.stringify(message),
   "muteHttpExceptions": true,
   "headers": {
      "Authorization": 'Bearer ' +  service.getAccessToken(),
      "Content-Type" : 'application/json; charset=UTF-8'
    }
  }
  var rs = UrlFetchApp.fetch(url,options);
  
  return rs;
}



/**
 * Check thread information that already exists
 *
 * @param {string} spreadsheetId
 * @param {string} rangeName
 * @param {string} userName
 * @param {string} spaceName
 * 
 * @return {object} threadInfo from spreadsheet
 */

function getTheadInfo(spreadsheetId, rangeName, userName, spaceName, threadName) {

  var values = Sheets.Spreadsheets.Values.get(spreadsheetId, rangeName).values;

  if (!values) {
    console.log('Not found thread Info.');
    
  } else {
  
    for (var row = 0; row < values.length; row++) {

      if (threadName){
      // threadNameが指定されている場合は、TreadNameで検索する。
      // threadがすでに作られているか確認する場合に使う。
    
        if (values[row][4] == threadName ){
          return values[row];
        }
      } else {
      // threadNameが指定されていない場合は、userNameとspaceNameで検索する。
      // 管理者からユーザーにメッセージを返す場合に、ユーザーのスレッドを探す場合に使う。
        if (values[row][0] == userName && values[row][3] == spaceName){
          console.log(values[row])
          return values[row];
        }
      }
    }
    
    console.log('Not match thread Info.');
  }
}



/**
 * get ManageInfo
 *
 * @param {string} spreadsheetId
 * @param {string} rangeName
 * @param {string} userName
 * @param {string} spaceName
 * 
 * @return{object} manageUserInfo From spreadSheet
 */
function getManageUserInfo(spreadsheetId ,rangeName, userName, spaceName) {

  var values = Sheets.Spreadsheets.Values.get(spreadsheetId, rangeName).values;
  
  if (!values) {
    console.log('Not found Manager Info.');
  } else {
    for (var row = 0; row < values.length; row++) {
      if (values[row][0] == userName && values[row][1] == spaceName){
        return values[row];
      }
    }
    console.log('Not Match Manager Info.')
  }
}


/**
 * append values to target spreadsheet
 *
 * @param {string} spreadsheetId
 * @param {string} rangeName
 * @param {Object} values
 */
function appendValuesSpreadsheet(spreadsheetId, range, values) {
  
  
  var valueRange        = Sheets.newRowData();
  valueRange.values     = values;

  var appendRequest     = Sheets.newAppendCellsRequest();
  appendRequest.sheetId = spreadsheetId;
  appendRequest.rows    = [valueRange];


  var rs = Sheets.Spreadsheets.Values.append(valueRange, spreadsheetId,range , {
    valueInputOption: "USER_ENTERED"
  });
  
}

/**
 * Upload Attachment To GoogleDrive
 *
 * @param  {Object} attachement
 * @param  {Object} folderId
 * @param  {Object} fileName 
 *
 * @return {string} GoogleDriveURL
 */
 
function uploadAttachmentToDrive(attachment, folderId, fileName){

  var service       = getOAuth2Service(); 
  var resourceName  = attachment[0].attachmentDataRef.resourceName;
  var blob          = "";
  var folder        = "";
  var uploadFile    = "";
    
  var url           = "https://chat.googleapis.com/v1/media/" + resourceName + "?alt=media";
  var rs = UrlFetchApp.fetch(url, {
    headers: {
      'Authorization': 'Bearer ' + service.getAccessToken(),
    },
    'muteHttpExceptions': true,
  });

  if (rs.getResponseCode() != 200) {
    return "Failed to get file content with error code: " + rs.getResponseCode();
  }

  blob       = rs.getBlob();
  folder     = DriveApp.getFolderById(folderId);
  uploadFile = folder.createFile(blob);
  
  uploadFile.setName(fileName);
  
  return uploadFile.getUrl();
}





/**
 * Creates OAuth2 Service to use chat api
 *
 * @return {Object} contains OAuth2 token
 */

function getOAuth2Service() {
  
  var jsonKey                   = JSON.parse(PropertiesService.getScriptProperties().getProperty("JSON_KEY"));    
  var serviceAccountPriveKey    = jsonKey.private_key;
  var serviceAccountClientEmail = jsonKey.client_email; 
  var scope = 'https://www.googleapis.com/auth/chat.bot';
  var service = OAuth2.createService('chat')
      .setTokenUrl('https://accounts.google.com/o/oauth2/token')
      .setPrivateKey(serviceAccountPriveKey)
      .setClientId(serviceAccountClientEmail)
      .setPropertyStore(PropertiesService.getUserProperties())
      .setScope(scope);
  
  if (!service.hasAccess()) {
    console.log('Authentication error: %s', service.getLastError());
    return;
  }
  
  return service;
}

####補足
1.管理用のスプレッドシートについて
2つのシートがあって、それぞれ下記のような構成だよ。
・threadInfo・・PGMで自動読み書きされる。
・manageUser・・先にこちらで設定する。管理者(回答者)とその管理スペースの情報を入力しておく。
*dispUserNameとかはこのシートから取得しなくてもできるんだけれど、回答者の名前を統一させたいなーと思って、
 こっちで管理するようにしたよ。

スクリーンショット 2020-08-12 20.02.27.png スクリーンショット 2020-08-12 20.06.32.png

2.スクリプトプロパティについて
定数、かつ、あまりお外に見せたくない下記の四項目については、スクリプトプロパティにおいたよ。
・SPREADSHEET_ID
・FOLDER_ID
・SPACE_NAME
・JSON_KEY

##実際どんな感じで動くの??

【登場人物】
Bot ・・systemBot
回答者・・akky-tys :relaxed:
回答者のいるチャットルーム・・Bottest
困っているユーザー・・ neko スクリーンショット 2020-08-12 20.55.46 1.png

【シナリオ】
####Neko → systemBot : DMを送る
スクリーンショット 2020-08-12 20.57.53.png

####systemBot : チャットルーム(Bottest)に転送
スクリーンショット 2020-08-12 20.58.02.png

####:relaxed: → systemBot : チャットルーム(Bottest)で同一スレッドにmention付きのメッセージを送る
スクリーンショット 2020-08-12 20.58.18.png

####systemBot → Neko : メッセージをDMで転送
スクリーンショット 2020-08-12 20.58.44.png

####Neko → systemBot : 画像付きDMを送る
スクリーンショット 2020-08-12 20.59.22.png

systemBot : チャットルーム(Bottest)に画像のGoogleDriveへのリンクを貼って転送
スクリーンショット 2020-08-12 20.59.42.png

ふふふ・・動いているよ・・・:relaxed:

###課題
1.メッセージを編集、削除した場合にonMassageが効かない
→ 公式のsupportからIssueTrackerを見たけれど、現在は編集や削除を拾う機能がないみたい。
  リクエストはすでに出ていたので、追加されたら、組み込むよ。
2.カードのサイズがちっちゃい。
→ これもまだ編集が出来なさそう。Card形式のメッセージにして見たけれど、
  文章量が多いとぎゅっとしちゃってなんだかなあって感じだったよ。
3.GoogleSiteとの連携 
→ これやりたかったんだけれど、まだAPIが旧Siteにしか対応していないみたいだった・・。

###感想
GoogleChatを使う前はSlackを使っていて、そっちでもBotを作ったことがあるので比較してだけれど、
Botを作るにしても、APIにしても、参考文献にしても、Googleの方はまだ発展途中だなあという印象を受けたよ。
あとはBotの設定や認証でひっかっかることが多かったな。多分理解が浅いのかも。
GASも初めて触ったけれど、:relaxed:でもできるレベルの分かりやすさで助かったよ・・。
Google系のサービスを組み合わせられるのが面白いので、もっといろいろ試してみたいなって思ったよ!

##参考にさせていただいたもの
ふふふ、公式:relaxed: ありがとう大好き。
公式:GoogleAppsScript
公式:GoogleChatAPI
公式:GoogleSheetsAPI

###細かい説明はこっちを読んでもらえると嬉しいよ
GoogleChat の一番簡単なBotを公式通り作ってみるよ
GoogleChat のREST APIを叩いてみるよ スペース情報をとってくる編
GoogleChat のREST APIを叩いてみるよ スペース情報をとってくる編
GoogleSheet APIで読み込み、書き込みをしてみるよ
GoogleChatでBotからCard形式のメッセージを送ってみるよ
Google ChatでBotに送った添付ファイルをDriveAppを使って保存するよ

5
2
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
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?