サイバーホルンのブログCyberhorn's Blog

ChatworkをGoogle Chatに転送するGASを作る①(chatworkからのメッセージ取得&スプレッドシート保存)

  • 2021.09.09
  • 2021.09.08

データ×マーケティング

こんにちは、Cyberhorn エンジニアです。先日、こちらの記事で、「GASを使って、Google Analytice (GA) の数値報告をするbotを作ってみた」件を紹介しました。

GASを使ってGoogle AnalyticsデータをChatworkに自動通知するbotを作ってみた

GASを使ってGoogle AnalyticsデータをChatworkに自動通知するbotを作ってみた
弊社のデータチームでは、HPを運用するにあたってGoogle Analyticsでユーザー行動を計測しています。 例えば、 「サイトの訪問者数はどれくらいか」「...

今回は、もう少し込み入ったアプリケーションとして、「GASをもちいて、Chatwork を Google Chat に転送する」というものを紹介します。

Chatwork を Google Chat に転送するアプリを作った背景

弊社では、社内のチャットアプリとして「Chatwork」を使用していました。

先日、社内のさらなる業務効率化のために、Google Workspace を導入することになりました。それに伴って、社内のチャットツールもGoogleに一本化し、Google 謹製の Google Chat を活用していこう、というものです。

チャットアプリを乗り換えるのは大変で、それまで続いていたやり取りがぶった切られてしまいます。

また、古い方のチャットアプリの過去のメッセージから、いろいろな情報を探すこともあるでしょう。

そこで、チャットアプリの移行措置として、「一定期間、Chatwork のトークルームに届いたメッセージを、それに対応する Google Chat 側のトークルームに自動転送し、目の前の業務をするために必要な情報が Google Chat にある程度溜まった段階で、ユーザーを新しい方のチャットアプリに誘導する」という作戦をとりました。

Google Chat のよいところ

チャットをスレッド化できる

Chatwork では、どのメッセージに対してリプライしているのか明示する機能はあるものの、やり取りが続いているチャットがツリーとしてまとまった視覚化がなされているわけではありません。

それによって、特にテレワークの際、複数の相談事項や依頼事項があるときに、メッセージを追いかけるのが大変になります。Google Chat では、チャットルーム開設時に設定することで、メッセージをスレッド化できるので、メッセージの流れがすっきりすることを期待しています。

メッセージ・タスク・ファイルの統合的管理

2つ目は、メッセージと、タスクと、ファイルを統合的に管理することができる点です。

なぜチャットアプリを使うのか? という部分を掘り下げると「チームで仕事を進める」ためです。

そして、チームで仕事を進める上では、やるべきことを分解した「タスク」と、タスクの結果である成果物が「ファイル」として、チャット上でやりとりされます。

Google Chat は、残りやるべきタスクが見やすく、ファイルが一覧になって探しやすいところがいいなと思っています。

カレンダー・他 Google アプリとの連携

これは Google Workspace 導入に伴うメリットですが、業務で使うアプリケーションを統一することにより、Google サービスとして横断的に連携させて使うことができます。

弊社では、インターン生が複数名在籍しており、インターン生へのタスク依頼の際は、「〇〇ちゃんにこの作業をしてほしいのだけど、今日勤務するのかな?」とカレンダーを見に行くケースが多いです。

Google Chat なら、Chat の画面からカレンダーを閲覧・操作することが可能なので、とてもありがたいです。

また、Google系のドキュメントなら、Google Chat のアプリ上から閲覧、操作することもできるので、ブラウザのタブをいったりきたりすることも減ります。

GAS による bot の容易な実装

Google Chat を利用する最も大きいメリットは、これにあると考えています。

Chatwork は API の提供があるものの、GAS から操作するには少々面倒な部分があったり、認証まわりで少し不安な部分がありました。

弊社では、Google BigQuery や Google Analytics をはじめ、Google系のプラットフォーム上にある数値をチャットアプリ上で報告したい場合が多いです。この場合は GAS を使うことが多いので、アウトプット先のチャットアプリも Google 系だと何かと助かります。

Chatwork → Google Chat 転送システムの概要・仕様

上記のような Google Chatの特徴をふまえて、私が作った転送システムの仕様は下記のようなものです。

  • チャットワークには、いくつものトークルームがある。それと同じ構成なトークルームを Google Chat 側にも作成し、対応するトークルームにメッセージ転送する
  • Google Chat のスレッド機能を活用し、チャットワークでやり取りされた1日分のメッセージを、1つのスレッドにまとめる
  • チャットワークAPIで取得したメッセージのレコードをスプレッドシートに記入し、スプレッドシート上のレコードを読み取って Google Chat に転送する
  • GAS のトリガーで処理を定期実行させる

Chatwork → Google Chat 転送のGASコード

それでは、実際に私が作ったコードを紹介していきます。

下準備 : V8ランタイムを有効化する

まずは、下記のGAS画面の下記部分にチェックを入れて、V8ランタイムを有効化します。

これにより、クラスを定義したり、 V8 ランタイム下で使える便利な文法を利用することができるようになります。

V8 ランタイムについての説明は本記事の範囲を超えてしまうので、下記のような記事を参考ください。

GASのV8ランタイムで使えるECMAScript構文のまとめ

よく利用するスプレッドシートの操作のメソッド化

この転送システムでは、スプレッドシート上でメッセージのレコードを管理しているので、よく使うスプレッドシート操作をメソッド化しています。

function sheetRangeToFlatArray(sheet_id, table_header) {
  const sh = SpreadsheetApp.openById(sheet_id).getSheetByName(table_header["sheet_name"]);
  const arr_sheet_columns = sh.getRange(table_header["range"]).getDisplayValues().flat();
  return arr_sheet_columns;
}

function columnRecordsToArray(column_name, sheet_id, table_header) {
  const sh = SpreadsheetApp.openById(sheet_id).getSheetByName(table_header["sheet_name"]);
  const arr_sheet_columns = sh.getRange(table_header["range"]).getDisplayValues().flat();
  const active_row = sh.getRange(table_header["range"]).getRow();
  const arr_target_column =
    sh.getRange(active_row, arr_sheet_columns.indexOf(column_name) + 1, sh.getLastRow() - active_row + 1).getDisplayValues().flat();

  return arr_target_column.slice(1); //カラムのヘッダーの値を取り除いている
}

function lastRowOfTargetColumn(target_column_name, sheet_id, table_header) {
  const sh = SpreadsheetApp.openById(sheet_id).getSheetByName(table_header["sheet_name"]);
  const arr_sheet_columns = sh.getRange(table_header["range"]).getDisplayValues().flat();
  const target_column_index = 1 + arr_sheet_columns.indexOf(target_column_name);
  const is_2nd_row_empty = sh.getRange(2, target_column_index).getValue() === ""
  if (is_2nd_row_empty) {
    return 1;
  }
  const last_row
    = sh.getRange(1, target_column_index).getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow();

  return last_row;
}

function twoColumnsToKeyedObj(key_col_name, val_col_name, sheet_id, table_header) {
  const arr_key = columnRecordsToArray(key_col_name, sheet_id, table_header);
  const arr_val = columnRecordsToArray(val_col_name, sheet_id, table_header);
  const keyed_obj = {};

  for (let i = 0; i <= arr_key.length; i++) {
    keyed_obj[arr_key[i]] = arr_val[i];
  }
  return keyed_obj;
}

APIトークンと、スプレッドシート上のテーブル情報に関するクラス

class ConfigOfCw {

  constructor() {
    this.cw_api_token = '*****************' //自分用の chatwork APIトークンを記入
    this.sheet_id = '******************';  //作業対象のスプレッドシートIDを記入
    // table_header の prefix がつく object は、seet_name および range のプロパティを持つ
    this.table_header_room_info = { "sheet_name": "config", "range": "A2:D2" };
    this.table_header_messages = { "sheet_name": "messages", "range": "A1:H1" };

    this.room_ids = columnRecordsToArray('room_id', this.sheet_id, this.table_header_room_info);
  };
}

chatworkAPI エンドポイントに、メッセージ取得のための GET リクエスト送信するクラス

class HttpGetMethodToCw {

  constructor(url, cw_api_token) {
    this.url = url;
    this.headers = { "X-ChatWorkToken": cw_api_token };
    this.options = { 'method': 'GET', 'headers': this.headers };
    this.response = UrlFetchApp.fetch(this.url, this.options);
    this.is_new_messege_exists = this.response.getContentText().length !== 0;
    this.new_messege = this.newMessege();
  }

  newMessege() {
    if (this.is_new_messege_exists) {
      this.json_content = JSON.parse(this.response.getContentText());
      this.api_info = this.response.getHeaders();
      this.api_reset_date
        = Utilities.formatDate(
          new Date(this.api_info['x-ratelimit-reset'] * 1000), 'Asia/Tokyo', 'yyyy/MM/dd/HH:mm:ss'
        );
      this.result = this.json_content;
    }
  }
} 

チャットワークから取得したメッセージを、フォーマット化してスプレッドシートのテーブルに書き込むためのクラス

class WriteMessegeFromCw {

  constructor(obj_messeges, room_id, sheet_id, table_header) {
    this.room_id = room_id;
    this.obj_messeges = obj_messeges;
    this.sheet_id = sheet_id;
    this.table_header = table_header;
  }

  execute() {
    for (const obj_messege of this.obj_messeges) {
      const formatted_obj = this.format(obj_messege);

      if (this.isNewMessege(formatted_obj)) {
        this.insertRecordToSheet(formatted_obj);
        Logger.log(formatted_obj);
      }
    }
  }

  format(obj_messege) {
    const formatted_obj = {};

    formatted_obj.room_id = this.room_id;
    formatted_obj.message_id = obj_messege["message_id"];
    formatted_obj.sender_name = obj_messege["account"]["name"];
    formatted_obj.send_time =
      Utilities.formatDate(
        new Date(obj_messege["send_time"] * 1000), 'Asia/Tokyo', 'yyyy/MM/dd HH:mm:ss'
      );
    formatted_obj.send_date =
      Utilities.formatDate(
        new Date(obj_messege["send_time"] * 1000), 'Asia/Tokyo', 'yyyy/MM/dd'
      );
    formatted_obj.update_time =
      obj_messege["update_time"] == 0
        ? 0
        : Utilities.formatDate(
          new Date(obj_messege["update_time"] * 1000), 'Asia/Tokyo', 'yyyy/MM/dd/HH:mm:ss'
        );
    formatted_obj.body =
      `Date : ${formatted_obj.send_time}\n`
      + `From : ${formatted_obj.sender_name} \n`
      + `------------\n`
      + obj_messege["body"];
    formatted_obj.is_first_of_the_day
      = this.is_first_of_the_day(this.sheet_id, this.table_header, formatted_obj.send_date);

    return formatted_obj;
  }

  is_first_of_the_day(sheet_id, table_header, send_date) {
    const values_room_id = columnRecordsToArray("room_id", sheet_id, table_header);
    const values_send_date = columnRecordsToArray("send_date", sheet_id, table_header);

    for (let i = 0; i <= values_room_id.length; i++) {
      if (values_send_date[i] === send_date && values_room_id[i] === this.room_id) {
        return false;
      }
    }
    return true;
  }

  isNewMessege(formatted_obj) {
    const message_ids = columnRecordsToArray("message_id", this.sheet_id, this.table_header);
    return message_ids.indexOf(formatted_obj.message_id) === -1
  }

  insertRecordToSheet(formatted_obj) {
    const sh =
      SpreadsheetApp.openById(this.sheet_id).getSheetByName(this.table_header["sheet_name"]);
    const columns = sh.getRange(this.table_header["range"]).getDisplayValues().flat();
    const last_row = sh.getLastRow() + 1;

    for (const key of Object.keys(formatted_obj)) {
      const target_range = sh.getRange(last_row, columns.indexOf(key) + 1);
      target_range.setNumberFormat('@'); //スレッドシートの表示形式が自動変換されるのを防ぐ
      target_range.setValue(formatted_obj[key]);
    }
  }
}

チャットワークからメッセージ取得するメインの関数

function getCwMain() {
  const config = new ConfigOfCw();

  for (const room_id of config.room_ids) {
    const url = "https://api.chatwork.com/v2/rooms/" + room_id + "/messages?force=0";
    const get_chatwork = new HttpGetMethodToCw(url, config.cw_api_token);

    if (get_chatwork.is_new_messege_exists) {
      const obj_messeges = get_chatwork.result;
      const write_messeges
        = new WriteMessegeFromCw(obj_messeges, room_id, config.sheet_id, config.table_header_messages).execute();
    }
  }
}

長くなったので、「スプレッドシートに保存した chatwork のメッセージのレコードを、Google Chat に送信する」処理については、次回の記事で紹介します。

ChatworkをGoogle Chatに転送するGASを作る①(chatworkからのメッセージ取得&スプレッドシート保存)

ChatworkをGoogle Chatに転送するGASを作る①(chatworkからのメッセージ取得&スプレッドシート保存)
こんにちは、Cyberhorn エンジニアです。先日、こちらの記事で、「GASを使って、Google Analytice (GA) の数値報告をするbotを作っ...

  • 2021.09.09
  • 2021.09.08

データ×マーケティング

広告代理店/制作会社の方へ

無料アカウント診断サービス