LineメッセージをSlackへ転送させるスクリプト

# LineメッセージをSlackへ転送させるスクリプト

## 主な機能
- LineメッセージをSlackへ転送
- スプレットシートへフォロユーザーのID、グループIDを追記
- グループメッセージをSlackチャネルへ振り分け（スプレットシートに事前設定が必要）
- 受信ファイルをGoggleDriveへアップロード

## 環境変数説明

 環境変数名 | 説明 | 参考
 :---------|:----|:------
 CHANNEL_ACCESS_TOKEN | Line Apiへのアクセストークン | -
 SLACK_LEGACY_TOKEN | Slackのレガシートークン | Slackのトークンについては [こちら](https://qiita.com/ykhirao/items/3b19ee6a1458cfb4ba21) を参照
 SPREADSHEET_KEY | スプレットシートへのキー | スプレットシートキーについては [こちら](https://qiita.com/negito6/items/c64a7a8589faaffcfdcf) を参照
 SHEET_NAME | アクセスするスプレットシートのシート名 | -
 SYSTEM_NOTIFICATION_CHANNEL | 通知先として設定するSlackのチャンネル名 | -
 NOTIFICATION_CHANNEL | 個別メッセージ、未設定グループメッセージの通知先 Slackチャンネル名 | -
 SYSTEM_ICON | システムアナウンス時の表示アイコン（URL） | -
 LINE_UPLOAD_FORDER | Lineで送られてきたファイルをアップロードするフォルダ名 | -


## 実装コード
```javascript
/** @type {string} line チャンネルアクセストークン */
var CHANNEL_ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty('CHANNEL_ACCESS_TOKEN');
/** @type {string} Slack レガシートークン */
var SLACK_LEGACY_TOKEN = PropertiesService.getScriptProperties().getProperty('SLACK_LEGACY_TOKEN');

/** @type {object} スプレットシートオブジェクト */
var SPREADSHEETS = SpreadsheetApp.openById(PropertiesService.getScriptProperties().getProperty('SPREADSHEET_KEY'));
/** @type {object} [description] */
var SHEET = SPREADSHEETS.getSheetByName(PropertiesService.getScriptProperties().getProperty('SHEET_NAME'));
/** @type {array} sheetデータが格納された配列 */
var SHEET_DATA = SHEET.getDataRange().getValues();


/**
 * Postを受け付けるメソッド
 * @method doPost
 * @param  {object} e イベントデータ
 * @return {Int}    戻り値（使用しないので0を返す）
 */
function doPost(e) {
  /** @type {string} メッセージの通知先 */
  var SYSTEM_NOTIFICATION_CHANNEL = PropertiesService.getScriptProperties().getProperty('SYSTEM_NOTIFICATION_CHANNEL');
  /** @type {string} メッセージの通知先 */
  var NOTIFICATION_CHANNEL = PropertiesService.getScriptProperties().getProperty('NOTIFICATION_CHANNEL');

  var line = JSON.parse(e.postData.contents).events[0];
  console.log(JSON.stringify(line));
  switch (line.type) {
    case 'follow':
      var profile = get_line_profile(line);
      idRecordsRegistration(line, profile, '', 1, 1);
      postSlackMessage(SYSTEM_NOTIFICATION_CHANNEL, '```フォローしました。\nステータスメッセージ: ' + profile.statusMessage + '```', profile);
      break;
    case 'unfollow':
      var rowNo = searchIdFromSpreadSheet(line.source.userId);
      if (rowNo == undefined) {
        postSlackMessage(SYSTEM_NOTIFICATION_CHANNEL, '```ブロックされましたが、該当IDが見つかりませんでした。\nID: ' + line.source.userId + '\n```');
      } else {
        SHEET.getRange("E"+(rowNo+1)).setValue(0);
        SHEET.getRange("F"+(rowNo+1)).setValue(0);
        var userName = getValFromCell(rowNo, 0);
        postSlackMessage(SYSTEM_NOTIFICATION_CHANNEL, '```ブロックされました。\nNAME: ' + userName + '\n```');
      }
      break;
    case 'join':
      idRecordsRegistration(line, undefined, '', 1, 0);
      postSlackMessage(SYSTEM_NOTIFICATION_CHANNEL, 'グループに追加されました。\n登録情報を確認してください。 ' + line.source.groupId);
      break;
    case 'leave':
      var groupInfo = getValFromCell(searchIdFromSpreadSheet(line.source.groupId), 0);
      if (groupInfo == '') {
        postSlackMessage(SYSTEM_NOTIFICATION_CHANNEL, 'グループから外されましたが、該当IDが見つかりませんでした。\n ' + line.source.groupId);
      } else {
        SHEET.getRange("E"+(rowNo+1)).setValue(0);
        SHEET.getRange("F"+(rowNo+1)).setValue(0);
        postSlackMessage(SYSTEM_NOTIFICATION_CHANNEL, 'グループから外されました。\n ' + groupInfo);
      }
      break;
    case 'message':
      var profile = get_line_profile(line);
      switch (line.source.type) {
        case 'user':
          postSlackMessage(NOTIFICATION_CHANNEL, get_line_message(line), profile);
          break;
        case 'group':
        case 'room':
          var groupInfo = getValFromCell(searchIdFromSpreadSheet(line.source.groupId), 0);
          var sendCh = getValFromCell(searchIdFromSpreadSheet(line.source.groupId), 3);
          if (groupInfo == '') {
            groupInfo = line.source.groupId;
          }
          if (sendCh == '') {
            sendCh = NOTIFICATION_CHANNEL;
          }
          postSlackMessage(sendCh, get_line_message(line), profile, groupInfo);
          break;
      }
      break;
    default:
      postSlackMessage(SYSTEM_NOTIFICATION_CHANNEL, line);
      break;
  }
  return 0;
}

/**
 * lineメッセージ内容毎の仕分け処理
 * @method get_line_message
 * @param  {object}     line line eventオブジェクト
 * @return {string}        メッセージ内容 or GDriveへアップロしたオブジェクトUrl
 */
function get_line_message(line) {
  switch (line.message.type) {
    case 'text':
      return line.message.text;
    case 'image':
      var blob = get_line_content(line.message.id);
      return uploadToGoogleDrive(line.message.id + '.png', blob, 'image/png');
    case 'video':
      var blob = get_line_content(line.message.id);
      return uploadToGoogleDrive(line.message.id + '.mp4', blob, 'video/mp4');
    case 'audio':
      var blob = get_line_content(line.message.id);
      return uploadToGoogleDrive(line.message.id + '.m4a', blob, 'audio/aac');
    case 'file':
      var blob = get_line_content(line.message.id);
      return uploadToGoogleDrive(line.message.fileName, blob);
    case 'location':
      return '位置情報を受信\naddress: ' + line.message.address + '\nlatitude: ' + line.message.latitude + '\nlongitude: ' + line.message.longitude;
    case 'sticker':
      var timestamp = new Date();
      return 'https://stickershop.line-scdn.net/stickershop/v1/sticker/' + line.message.stickerId + '/android/sticker.png?' + timestamp.getTime();
    default:
      return 0;
  }
}

/**
 * lineからコンテント取得処理
 * @method get_line_content
 * @param  {string}     message_id メッセージID
 * @return {object}          送信されたオブジェクト情報
 */
function get_line_content(message_id) {
  try {
    var headers = {
      'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN
    };
    var options = {
      'headers': headers
    };
    var url = 'https://api.line.me/v2/bot/message/' + message_id + '/content';
    var blob = UrlFetchApp.fetch(url, options);

    return blob;
  } catch (e) {

    console.log(e);
    return null;
  }
}

/**
 * lineからプロファイル情報取得
 * @method get_line_profile
 * @param  {object}     line line eventオブジェクト
 * @return {object}        プロファイルオブジェクト
 */
function get_line_profile(line) {
  var headers = {
    'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN
  };
  var options = {
    'headers': headers
  };
  var url;
  switch (line.source.type) {
    case 'user':
      url = 'https://api.line.me/v2/bot/profile/' + line.source.userId;
      break;
    case 'group':
      url = 'https://api.line.me/v2/bot/group/' + line.source.groupId + '/member/' + line.source.userId;
      break;
    case 'room':
      url = 'https://api.line.me/v2/bot/room/' + line.source.groupId + '/member/' + line.source.userId;
      break;
  }
  var response = UrlFetchApp.fetch(url, options);
  var content = JSON.parse(response.getContentText());
  return content;
}

/**
 * slackへのメッセージ送信処理
 * @method postSlackMessage
 * @param  {string}     sendCh    送信先チャネル名
 * @param  {string}     mes       メッセージ情報
 * @param  {object}     profile   プロファイルオブジェクト情報
 * @param  {object}     auxiliary 補足情報'(グループ名とか)'
 * @return {object}         なし
 */
function postSlackMessage(sendCh, mes, profile, auxiliary) {
  /** @type {string} システムアイコン */
  var SYSTEM_ICON = PropertiesService.getScriptProperties().getProperty('SYSTEM_ICON');

  var slackApp = SlackApp.create(SLACK_LEGACY_TOKEN);
  if (profile != undefined) {
    var options = {
      username: profile.displayName + (auxiliary != undefined ? ' from ' + auxiliary : ''),
      icon_url: profile.pictureUrl
    };
  } else {
    var options = {
      username: 'system announce',
      icon_url: SYSTEM_ICON
    };
  }

  console.log(JSON.stringify(options));
  slackApp.postMessage(sendCh, mes, options);
}

/**
 * G Driveからフォルダ名に一致するフォルダIDを取得する
 * @method getAllFolderID
 * @param  {string}     folderName フォルダ名
 * @return {string}          フォルダID
 */
function getAllFolderID(folderName) {
  var all_folders = DriveApp.getFolders();

  while (all_folders.hasNext()) {
    var folder = all_folders.next();
    if (folderName == folder.getName()) {
      return folder.getId();
    }
  }

  console.log('not unmatch folder name:' + folderName);
  return null;
}

/**
 * G Driveへファイルのアップロード処理
 * @method uploadToGoogleDrive
 * @param  {string}            fileName ファイル名
 * @param  {object}            blob     blobオブジェクト（ファイルオブジェクト）
 * @param  {string}            fileType ファイルMIMEタイプ
 * @return {string}                     ファイルへのアクセスURL
 */
function uploadToGoogleDrive(fileName, blob, fileType) {
  /** @type {string} g Drive上のアップロード先 */
  var LINE_UPLOAD_FORDER = PropertiesService.getScriptProperties().getProperty('LINE_UPLOAD_FORDER');

  var file = null;
  try {
    var folderId = getAllFolderID(LINE_UPLOAD_FORDER);
    if (folderId != null) {
      var fileOption = {
        title: fileName,
        parents: [{
          id: folderId
        }]
      };
      if (fileType != undefined) fileOption.mimeType = fileType


      file = Drive.Files.insert(fileOption, blob);
      return file.thumbnailLink?file.thumbnailLink:file.embedLink;

    } else {
      file = DriveApp.createFile(blob);
      return file.getThumbnail()!=null?file.getThumbnail():file.getUrl();
    }
  } catch (e) {
    return e.message;
  }
}

/**
 * ID情報の登録処理（Spread sheetへの書き込み）
 * @method idRecordsRegistration
 * @param  {object}              line       lineメッセージオブジェクト
 * @param  {object}              profile    プロファイル情報 無い場合はundefined
 * @param  {string}              sendCh     送信するチャンネル
 * @param  {integer}             followFlg  フォローされているか判定情報
 * @param  {integer}             enabledFlg 有効無効フラグ
 * @return {none}                           なし
 */
function idRecordsRegistration(line, profile, sendCh, followFlg, enabledFlg) {
    var id = '';
    var name = '';

    switch (line.source.type) {
      case 'user':
        id = line.source.userId;
        break;
      case 'group':
      case 'room':
        id = line.source.groupId;
        break;
      default:
        console.log('未定義type');
        return ;
    }

    if (profile != undefined) {
        name = profile.displayName
    }

    SHEET.appendRow([name, id, line.source.type, sendCh, followFlg, enabledFlg]);
}

/**
 * スプレットシートデータから指定値が格納された行番号を返す
 * @method searchIdFromSpreadSheet
 * @param  {string}               val 検索値
 * @return {stirng}                   該当値が格納された行番号 無い場合はundefined
 */
function searchIdFromSpreadSheet(val) {
    for (var rowNo=0; rowNo<SHEET_DATA.length; ++rowNo) {
      if (SHEET_DATA[rowNo].indexOf(val) !== -1) {
        return rowNo;
      }
    }

    return undefined;
}

/**
 * スプレットシートデータの特定セルからデータを取り出すあ
 * @method getValFromCell
 * @param  {integer}      row 行番号
 * @param  {integer}      col 列番号
 * @return {string}           セルに格納されている情報
 */
function getValFromCell(row, col) {
  if (row == undefined || col == undefined) {
    return '';
  }

  return SHEET_DATA[row][col];
}

```

## 参考（Special Thanks)

- [Slack BotをGASでいい感じで書くためのライブラリを作った](https://qiita.com/soundTricker/items/43267609a870fc9c7453)
- [Google Apps Script(GAS)を用いてLINE Botが受信した情報をSlackに送る](https://okawa.routecompass.net/gas-line-slack/)