SlackとLINEを連携する(2) LINE→Slack
LINEに投稿されたメッセージをSlackにも通知するために、Google Apps Script(以下GAS)で処理します。
同時に、SlackからLINEへ通知する際に必要となるLINEユーザ情報をファイルに貯め込むことも必要です。
GASもjavascriptもほぼ初めてなので、妙な部分があったら優しく教えてください。
長いので要点だけ解説します。
ソースコード
/** * @fileoverview LINEボットに対して送られたメッセージをSlackのgeneralチャンネルに転送する。 * @author smp */ // LINEアクセストークン var LINE_ACCESS_TOKEN = '(秘密)' // Slack Webhook 用URL var SLACK_URL = 'https://hooks.slack.com/services/(秘密)'; // GoogleドライブディレクトリID var GOOGLE_DRIVE_DIR_ID = '(秘密)'; // LINE BOTフォロー時リプライメッセージ var LINE_FOLLOW_REPLY_MESSAGE = 'LINE Slack連携ユーザに登録しました。Slackの投稿がLINEに届き、LINEの投稿がSlackに届くようになります。'; // LINE返信用APIのURL var LINE_MESSAGE_REPLY_URL = "https://api.line.me/v2/bot/message/reply"; // LINEプロフィール取得APIのURL var LINE_PROFILE_URL = 'https://api.line.me/v2/bot/profile'; // LINEユーザ一覧ファイル名 var LINE_USER_LIST_FILENAME = 'LineUserList.json'; /** * @summary Postリクエスト処理。 * LINEボットからPOSTで通知されたタイミングで実行する関数。 * messageイベントの場合は、イベントに含まれるチャンネル名、ユーザ名、本文をSlackに通知する。 * followイベントの場合は、ユーザリストにユーザ情報を追加する。 * leaveイベントの場合は、ユーザリストからユーザ情報を削除する。 * @param e LINEから通知されるメッセージ */ function doPost(e) { // 各種情報を取得 var jsonData = JSON.parse(e.postData.contents); var event = jsonData.events[0]; try { switch (event.type) { case 'message': // メッセージ投稿時 switch (event.message.type) { case 'text': // ユーザーのメッセージを取得 // (1)返信用トークン var replyToken = event.replyToken; // (2)ユーザメッセージ var userMessage = event.message.text; // (3)ユーザID var userId = event.source.userId; // (4)タイムスタンプ var timestamp = event.timestamp; // ユーザIDからユーザ名を取得する。 var userName = getUserName(userId); // Slackにメッセージを送信する。 pushToSlack(userName, userMessage); break; default: // do nothing break; } break; case 'follow': // 友だち追加時 // (1)返信用トークン var replyToken = event.replyToken; // (2)タイムスタンプ var timestamp = event.timestamp; // (3)ユーザID var userId = event.source.userId; // ユーザIDからユーザ名を取得する。 var userName = getUserName(userId); // LINEユーザ一覧ファイルへユーザIDを追加する。 var addResult = addLineUserId(userId, userName, timestamp); if (addResult) { // 追加成功時、LINE BOTから返信メッセージを送る。 reply(LINE_FOLLOW_REPLY_MESSAGE, replyToken); } break; case 'unfollow': // ブロック時 // (1)タイムスタンプ var timestamp = event.timestamp; // (2)ユーザID var userId = event.source.userId; // ユーザIDからユーザ名を取得する。 var userName = getUserName(userId); // LINEユーザ一覧ファイルからユーザIDを削除する。 removeLineUserId(userId); break; default: // do nothing } } catch (err) { console.log(err); throw err; } } /** * @name LINEユーザ追加 * @summary LINEユーザ一覧ファイルにユーザ情報を追加する。 * @param {string} userId ユーザID * @param {string} userName ユーザ名 * @param {number} timestamp 登録日時( * @returns {boolean} 登録したか(true: 登録した、false: 登録しなかった) */ function addLineUserId(userId, userName, timestamp) { // 変数初期化 // (1)ユーザ一覧 var userList = []; // (2)保存するjsonデータを読み込み var jsonData = readJson(LINE_USER_LIST_FILENAME) // (3)新規登録ユーザ情報 var newUserData = { 'userId': userId, 'userName': userName, 'timestamp': timestamp } // 引数チェック if (userId == '') { return false; } // ユーザ一覧作成 // (1)既存ユーザ追加 if (jsonData.list) { for (var i in jsonData.list) { userList.push(jsonData.list[i]); // 新規ユーザと同じユーザIDが既に登録されていた場合は // ユーザを登録せずに終了する。 if (userId == jsonData.list[i].userId) { return false; } } } // (2)新規ユーザ追加 userList.push(newUserData); // 書き込むデータ生成 var newJsonData = { list: userList } // jsonデータ書き込み writeJson(LINE_USER_LIST_FILENAME, newJsonData); return true; } /** * @name LINEユーザ削除 * @summary LINEユーザ一覧ファイルからユーザ情報を削除する。 * @param {string} userId ユーザID * @returns {boolean} 削除したか(true: 削除した、false: 削除しなかった) */ function removeLineUserId(userId) { // 変数初期化 // (1)ユーザ一覧 var userList = []; // (2)保存するjsonデータを読み込み var jsonData = readJson(LINE_USER_LIST_FILENAME) // (3)ユーザを削除したか var removedFlg = false; // ユーザ一覧作成 if (jsonData.list) { for (var i in jsonData.list) { // ユーザIDが削除対象と異なる場合のみ、userListに追加 if (userId != jsonData.list[i].userId) { userList.push(jsonData.list[i]); } else { // 削除対象のユーザIDがユーザ一覧ファイルに存在した場合、 // 削除フラグをtrueにする。 removedFlg = true; } } } // ループ内で削除が一度も行われなかった場合、 // false(削除しなかった)をリターンして処理終了。 if (!removedFlg) { return false; } // 書き込むデータ生成 var newJsonData = { list: userList } // jsonデータ保存 writeJson(LINE_USER_LIST_FILENAME, newJsonData); return true; } /** * @name Google Drive内ファイル取得 * @summary Google Driveのディレクトリパスとファイル名から、ファイルを取得する。 * 存在しない場合は新しく作成する。 * @param {string} dirId ディレクトリID * @param {string} fileName ファイル名 * @returns ファイルオブジェクト */ function getGoogleDriveFile(dirId, fileName) { var dir = DriveApp.getFolderById(dirId); var file = dir.getFilesByName(fileName); if (file.hasNext()) { // 存在する場合 return file.next(); } else { // 存在しない場合 return dir.createFile(fileName, ''); } } /** * @name jsonファイル書き込み * @summary jsonファイルをGoogle Driveに書き込む。 * @param {string} jsonPath Google Drive内jsonファイルパス * @param {object} jsonData jsonデータ */ function writeJson(jsonPath, jsonData) { var content = JSON.stringify(jsonData); var file = getGoogleDriveFile(GOOGLE_DRIVE_DIR_ID, jsonPath); file.setContent(content); } /** * @name jsonファイル読み込み * @summary jsonファイルをGoogle Driveから読み込む。 * jsonファイルを読み込み、パースして返す。 * jsonファイルがない場合、新しくjsonファイルを作成する。 * @param {string} jsonPath Google Drive内jsonファイルパス * @returns {object} 読み込んだjsonデータ */ function readJson(jsonPath) { var file = getGoogleDriveFile(GOOGLE_DRIVE_DIR_ID, jsonPath); var content = file.getBlob().getDataAsString(); if (!content) { // ファイルの内容が空の場合 return {}; } else { var json = JSON.parse(content); return json; } } /** * @name LINEユーザ名取得 * @summary LINEユーザIDからLINEユーザ名を取得する。 * @description LINEユーザIDからLINEユーザ名を取得する。 * @param {string} userId LINEユーザID * @returns {string} LINEユーザ名 */ function getUserName(userId) { try { var url = LINE_PROFILE_URL + '/' + userId; var response = UrlFetchApp.fetch(url, { 'headers': { 'Authorization': 'Bearer ' + LINE_ACCESS_TOKEN } }); return JSON.parse(response.getContentText()).displayName; } catch (err) { // LINE APIのエラー発生時 console.log(err); return ''; } } /** * @name Slackにメッセージ通知 * @summary Slackにメッセージを送る。 * @param {string} userName Slackに表示されるユーザ名 * @param {string} text Slackに表示される本文 * @param {string} channel チャンネル名 先頭の#は不要 * @param {string} emojiIcon 絵文字アイコン名 例:':chart_with_upwards_trend:' * @returns fetchの結果 */ function pushToSlack(userName, text, channel, emojiIcon) { // payload作成 var payload = { 'username': 'Lineからの通知 ' + userName, 'text': text, }; if (channel) { payload['channel'] = '#' + channel; } if (emojiIcon) { payload['icon_emoji'] = emojiIcon; } var options = { 'method': 'post', 'contentType': 'application/json', 'payload': JSON.stringify(payload), }; // メッセージ送信 var response = UrlFetchApp.fetch(SLACK_URL, options); return response; } /** * @name LINE BOT返信 * @summary LINE BOTへの投稿に対して、LINE BOTに返信させる。 * @param {string} text 返信するメッセージ * @param {string} replayToken LINEから通知される返信用トークン * @returns fetchの結果 */ function reply(text, replyToken) { // ヘッダデータ作成。 var headers = { "Content-Type": "application/json; charset=UTF-8", 'Authorization': 'Bearer ' + LINE_ACCESS_TOKEN, }; // POSTリクエストデータ作成。 var postData = { "replyToken": replyToken, "messages": [ { 'type': 'text', 'text': text, } ] }; // 送信データを作成。 var options = { "method": "post", "headers": headers, "payload": JSON.stringify(postData) }; var response = UrlFetchApp.fetch(LINE_MESSAGE_REPLY_URL, options); return response; }
Postリクエスト受信と制御
doPost関数は特殊な関数で、GASをウェブアプリケーションとして公開した際に発行されるURLへPostリクエストを発行すると実行されます。
引数eにはLINEのアクション情報がjson形式で格納されています。詳しくはLINE Messaging APIのマニュアルを参照してください。
https://developers.line.biz/ja/reference/messaging-api/
e.postData.contentsをJSON.parseでパースして、イベント情報を取り出します。 全体の構成は以下のようになります。
function doPost(e) { // 各種情報を取得 var jsonData = JSON.parse(e.postData.contents); var event = jsonData.events[0]; try { switch (event.type) { case 'message': // メッセージ投稿時 switch (event.message.type) { case 'text': ... default: // do nothing break; } break; case 'follow': ... break; case 'unfollow': ... break; default: // do nothing } } ... }
switch caseで、event.typeの内容により場合分けをします。
# | event.type | LINEでの操作 | GASで実装する処理 |
---|---|---|---|
1 | message | BOTにメッセージ投稿 | Slack Incomming Webhook に対してメッセージ送信 |
2 | follow | BOTを友だち追加/ブロック解除 | LINEユーザ一覧ファイルにユーザ情報を追加 |
3 | unfollow | BOTをブロック | LINEユーザ一覧ファイルからユーザ情報を削除 |
Slackへ通知
pushToSlack
関数で実装してあります。
引数のchannelとemojiIconは設定できるので一応つけてますが、今回は使用していません。
function pushToSlack(userName, text, channel, emojiIcon) { // payload作成 var payload = { 'username': 'Lineからの通知 ' + userName, 'text': text, }; if (channel) { payload['channel'] = '#' + channel; } if (emojiIcon) { payload['icon_emoji'] = emojiIcon; } var options = { 'method': 'post', 'contentType': 'application/json', 'payload': JSON.stringify(payload), }; // メッセージ送信 var response = UrlFetchApp.fetch(SLACK_URL, options); return response; }
定数SLACK_URL
には、Slack Incomming Webhookを作成した際のURLを設定しておいてください。
主に実施していることはoptions
を作ることです。
method
どcontentType
は固定です。
payload
にはJson形式でSlackに投稿するメッセージを設定します。username
とtext
があればいいと思います。
仕様はSlackのSlack Incomming Webhookの設定画面に記載されています。
LINEユーザ一覧ファイルにユーザ追加
主にaddLineUserId
関数で実装しています。
LINEユーザ一覧ファイル(LineUserList.json)は以下のようなフォーマットにしました。 LINEユーザIDの一覧さえあればどのようなフォーマットでもいいです。
{ "list":[ { "userId":"Uxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "userName":"太郎", "timestamp":1561459938495 }, { "userId":"Uyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy", "userName":"次郎", "timestamp":1561460491870 } ] }
addLineUserId関数でユーザ情報を追加します。 1. readJson関数でLINEユーザ一覧ファイルから既存ユーザ情報(jsonData)を読み込み 1. 既存ユーザ情報(jsonData)と新規登録ユーザ情報(newUserData)を加えた新ユーザ情報(newJsonData)を作成 1. writeJson関数で新ユーザ情報(newJsonData)をLINEユーザ一覧ファイルに書き込み
function addLineUserId(userId, userName, timestamp) { // 変数初期化 // (1)ユーザ一覧 var userList = []; // (2)保存するjsonデータを読み込み var jsonData = readJson(LINE_USER_LIST_FILENAME) // (3)新規登録ユーザ情報 var newUserData = { 'userId': userId, 'userName': userName, 'timestamp': timestamp } // 引数チェック if (userId == '') { return false; } // ユーザ一覧作成 // (1)既存ユーザ追加 if (jsonData.list) { for (var i in jsonData.list) { userList.push(jsonData.list[i]); // 新規ユーザと同じユーザIDが既に登録されていた場合は // ユーザを登録せずに終了する。 if (userId == jsonData.list[i].userId) { return false; } } } // (2)新規ユーザ追加 userList.push(newUserData); // 書き込むデータ生成 var newJsonData = { list: userList } // jsonデータ書き込み writeJson(LINE_USER_LIST_FILENAME, newJsonData); return true; }
LINEユーザ一覧ファイルからユーザ削除
主にremoveLineUserId
関数で実装しています。
addLineUserId
とほぼ同じ処理になります。
ユーザ一覧作成時に、引数のuserId
と同一のIDが存在した場合は追加処理をスキップします。
function removeLineUserId(userId) { // 変数初期化 // (1)ユーザ一覧 var userList = []; // (2)保存するjsonデータを読み込み var jsonData = readJson(LINE_USER_LIST_FILENAME) // (3)ユーザを削除したか var removedFlg = false; // ユーザ一覧作成 if (jsonData.list) { for (var i in jsonData.list) { // ユーザIDが削除対象と異なる場合のみ、userListに追加 if (userId != jsonData.list[i].userId) { userList.push(jsonData.list[i]); } else { // 削除対象のユーザIDがユーザ一覧ファイルに存在した場合、 // 削除フラグをtrueにする。 removedFlg = true; } } } // ループ内で削除が一度も行われなかった場合、 // false(削除しなかった)をリターンして処理終了。 if (!removedFlg) { return false; } // 書き込むデータ生成 var newJsonData = { list: userList } // jsonデータ保存 writeJson(LINE_USER_LIST_FILENAME, newJsonData); return true; }
確認
先にLINEの設定をしておきます。
メニューバーから「公開」「ウェブアプリケーションとして導入」を選択し、ウェブアプリケーションとして公開します。
LINE Developersの設定を開き、「メッセージ送受信設定/Webhook URL」にウェブアプリケーションのURLを設定しておきます。
あとはLINE BOTをフォローして、メッセージを通知してSlackに表示されればOKです。
手こずったこと
- GAS(javascript)ではswitch文でcaseのたびにbreakが必要。慣れてる人にとっては普通でしょうが、最近のプログラミング言語では珍しくなりましたね。ハマりました。
- ログが確認しづらくデバッグに手間取りました。console.log()で画面から確認できるものの、画面に反映されるまでに時間がかかる…?ような気がします。
- デバッグ作業でSlackに投稿されてしまいました(すぐ消しました)。Slackにテスト用チャンネルを用意して、SLACK_URLを切り替えることで対処しました。
まだ続きます