IBM Cloud Functions でLINEとSlack連携させてみた

# はじめに
この記事は、LINEとIBM Cloud Functionsを連携したSlackへの通知機能を作ったので、3回に分けて紹介したい記事の第3回になります。
　第1回：[IBM Cloud Functons 動かしてみた](https://qiita.com/katahiro/items/4b515c12e42e98e17eb7)
　第2回：[IBM Cloud FunctionsでNode.jsのパッケージを利用してみた](https://qiita.com/katahiro/items/1d483030527367338574)
　第3回：[IBM Cloud Functions でLINEとSlack連携させてみた](https://qiita.com/katahiro/items/298eac9c0fa875333d84) ← この記事

## なんでLINEとSlackを連携させようと？？
近年、自社の社内コミュニケーションツールがSlackに移行したのですが、それ以前からチームメンバーの他愛のないやり取りをLINEで行っていました。協業するベンダーさんも含まれており、全員がSlackに移行するのは面倒(招待は本来可能)と思い、しかし現場を管理するマネージャーをLINEグループに招待したいかというと別の話。バカなやり取りを見せたいとは思わず、ただ、急な勤怠連絡が入るとLINEにいるメンバーは知っているけど、マネージャーが把握していないという状況は避けたい。結果、LINEで`勤怠連絡`だけ、社内のSlackに通知したら良いのではないかと思い、作りました。

## 必要なもの
- LINEアカウント
- Slackの目的のチャンネルに投稿可能なInoming WebhookのURL

# 何はともあれ、IBM Cloud Functionsの受け口を作成
LINEと連携する前に、LINEのメッセージを受取るFunctionsのAPIを作成しましょう。
前々回、前回の記事を読まれた方は余裕なはずですが、簡単に。
Functionsのトップから`アクション`を選んで、`作成`を選択しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/902dee9f-b5a4-5ac3-a9ef-518801dc8ed0.png)
作成対象で`アクション`を選択したら、`アクション名`を入力し、 `ランタイム`を選択、 `作成`を選択してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/9bbb9d2f-8078-fff1-26f7-619674deb69b.png)
1点だけ、自動生成されたコードに、呼び出された時の`params`ログを出力するように1行追加しておいてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/ca9e4cb4-bcfc-c263-8a00-f651a9a6f8b1.png)

```Javascript:コード
console.log(JSON.stringify(params));
```
APIとして公開するために、Functionsのトップから、`API`を選択して、以前作成したAPIを選択します。
APIが表示されたら`定義および保護`を選択し、`操作の作成`から新しいAPIを作成しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/05cc0771-3360-8293-c9d0-a149cd6197ab.png)
LINEからPOSTされるので、`verb`は`POST`を選択するように注意してください。あとは、適当に入力して、作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/de4080a1-77df-af74-98bb-7ed94d9f6c20.png)
これで、画面下部の`保存`を選択すれば、APIが公開されます。

# LINE側作業
## LINE Developers 
LINEでアプリ開発するなら必ず必要な [LINE developers](https://developers.line.biz/en/?status=success) のサイトがあるので、ご自身のラインアカウントでログインしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/b25d360a-2ab2-d5a2-38b0-a5944af8ca61.png)
ログインしたら、こんな感じの画面が出るので、`Create` でProviderを作成しましょう。
適当な名前を入れて`Create`しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/a8601004-83fd-1ccc-f73f-6939714cf01c.png)
`Create a Messaging API channel`を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/f458e9ec-9246-86f9-6a76-163f7a24cb17.png)
各種項目を入力して、`Create`を選択しましょう。
Channel type：そのまま
Provider：そのまま
Channel icon ：そのまま
Channel name：`かたひろのSlack通知` ※入力必須　マルチバイト文字推奨
Channel description：`katahiro-qiita` ※入力必須
Category：`個人` ※入力必須
Subcategory：`個人(その他)` ※入力必須
Email address：※入力必須
Privacy policy URL：不要
Terms of use URL：不要
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/57c975bb-3048-4c1e-008d-637ee550b14f.png)
このようにChannelが無事に作成できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/d702ceda-d3ba-792f-1384-52a882dcc9e9.png)

# Messaging APIの設定
各種設定をしてきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/58152198-dcdf-fbc5-4192-338bc41a4aae.png)
`Webhook URL`に先ほど作成したFunctionsのAPIのURLを設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/c3d224c5-f110-30ac-0684-8aec6e5c4c03.png)
最下部にある`Channel access token`は`Issue`を選択して発行しておいてください。後ほど利用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/c0d1f399-9334-2ca1-6ed7-106d41b45147.png)

# 動作検証
では、これで簡易な設定は完了したので、画面中ほどにあるQRコードをLINEで読み込んで、友達になりましょう。
では、「テスト」とメッセージを送ってみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/52360268-dd51-e9d9-619b-06ca49c34ff8.png)
自動応答の設定が残っているので、何かしらメッセージが返ってきますが、ここでは無視します。
ここで、IBM Cloud Functionsにはどのようなメッセージが届いているのでしょうか？
Functionsのトップから、`モニター`を選択すると、動作したAPIの情報が参照できます。
確認すると、LINEに「テスト」と送った時間に、アクティビティーログが出力されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/64cad6d3-b5a1-77b4-aac1-51319ab84b2e.png)
ここで、今回のアクティビティーログを開くと、アクション作成時に、`console.log(JSON.stringify(params))`を記述したメリットが出てきます。そう、LINEから受け取った全量がどのようなものなのか確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/9d3eafc8-d316-e46b-7125-d318e1b99fd8.png)
ヘッダー情報は無視して、LINEから受け取った`events`に限定してお見せすると、このような情報を受取っています。

```
{
  "events":[
    {
      "message":{
        "id":"12457265946945",
        "text":"テスト",
        "type":"text"
      },
      "mode":"active",
      "replyToken":"2af72141cd3f42319f30edd7e87dbc94",
      "source":{
        "type":"user",
        "userId":"U19ca3e54ebdd50e1fa26dxxxxx"
      },
      "timestamp":1596792352030,
      "type":"message"
    }
  ]
}
```
大事なのが、以下です。
`text`：もちろんLINEで投稿されたメッセージ
`replyToken`：応答メッセージを返す場合はこのTokenが必要になります
`userId`：LINEのAPIを利用して、メッセージを送ったユーザー名を取得する場合、API呼出しに必要になります

# LINEで応答
LINE developers側のMessaging API Settingsの画面下部に、下記の`LINE Official Account feature`の設定項目があるので、右側の`Edit`を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/77b975b8-3985-d7e1-0b65-ad821908c81c.png)

下記のような`LINE Official Account Manager`の画面が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/82b32e7a-6152-3d2e-5bad-fd54a3d0e249.png)

画面左部の`応答設定`のメニューから、`あいさつメッセージ`を`オフ`に、`応答メッセージ`を`オフ`にしましょう。これで、友達になった時に自動でメッセージが送られたり、メッセージを送ると自動で応答される、ということが無くなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/5aeaa259-1257-e986-6cc4-14b481e36827.png)
このように、自動応答メッセージも返答されなくなりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/36002e9d-41f1-b071-6984-998958781368.png)
さて、オウム返しのように、メッセージ内容を返してくれるように、アクションのコードを行いましょう。

```Javascript:package.json
{
  "name": "qiita-action",
  "version": "1.0.0",
  "main": "action.js",
  "dependencies": {
    "request": "^2.88.0"
  }
}
```
注意点としては、LINEのAPIを実行する時は、リクエストのヘッダーに`Authorization:Bearer`を設定して、先ほどの`Channel access token`を指定するようにしてください。

```Javascript:action.js
const main = async ( params ) => {
  // bearer には Channel access tokenを設定する
  const bearer = "HW9WPp3A33xxxxxxx"
  const lineReplyApiUrl = "https://api.line.me/v2/bot/message/reply";

  // LINEから来ている場合にのみ限定
  if(params.events && params.events[0].type === "message" ){
    
    // LINE情報取得
    const message = params.events[0].message.text;
    const replyToken = params.events[0].replyToken;
    const userId = params.events[0].source.userId;

    // LINE ユーザー名取得(1対1のトーク用)
    const getUserNameUrl = "https://api.line.me/v2/bot/profile/" + userId;
    const getUserNameOption = {
      method : "GET",
      url : getUserNameUrl,
      headers : {
        "Authorization" : "Bearer " + bearer,
        "Content-Type": "application/json",
      }
    }

    const userLineAPIObj = await callAPI(getUserNameOption);
    const userNameObj = JSON.parse(userLineAPIObj.body);
    const userName = userNameObj.displayName;
    
    // LINE返信
    const lineMessage = {
      "type" : "text",
      "text" : `${userName}さん、「${message}」と言いましたね`
    }
    const lineReplyOption = {
      method : "POST",
      url : lineReplyApiUrl,
      headers : {
        "Authorization" : "Bearer " + bearer,
        "Content-Type"  : "application/json",
      },
      json : {
        messages :[ 
          lineMessage
        ],
        replyToken : replyToken,
      }
    }
    const replyLineAPIObj = await callAPI(lineReplyOption);
    return true;
  }
  return true
}

const callAPI = (httpOption) => {
  return new Promise((resolve,reject) => {
    const request = require( 'request' );
    request( httpOption, ( err, res, buf ) => {
      if( err ){
        reject( { status: false, error: err } );
      }else{
        resolve(res);
      }
    });
  })
}

exports.main = main;
```
お見せ出来ませんが、私のLINEの登録名を取得した上で、取得したメッセージ内容も応答に含められていることが確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/085621e7-0320-58bf-730d-797b0ae2a014.png)

# Slackへ通知
後は、勤怠連絡の場合のみ、Slackに通知するように書き換えるだけです。

```Javascript:action.js
const main = async ( params ) => {
  // bearer には Channel access tokenを設定する
  const bearer = "HW9WPp3A33xxxxxxx"
  const lineReplyApiUrl = "https://api.line.me/v2/bot/message/reply";
  const incomingWebhookUrl = "https://hooks.slack.com/services/xxx/xxx/xxxxx";

  // LINEから来ている場合にのみ限定
  if(params.events && params.events[0].type === "message" ){
    // LINEメッセージを取得
    const message = params.events[0].message.text;
    // 勤怠連絡扱いのワードが含まれているかチェック
    if (message.match(/勤怠連絡/)){
      // LINE情報取得
      const replyToken = params.events[0].replyToken;
      const userId = params.events[0].source.userId;

      // LINE ユーザー名取得(1対1のトーク用)
      const getUserNameUrl = "https://api.line.me/v2/bot/profile/" + userId;
      const getUserNameOption = {
        method : "GET",
        url : getUserNameUrl,
        headers : {
          "Authorization" : "Bearer " + bearer,
          "Content-Type": "application/json",
        }
      }

      const userLineAPIObj = await callAPI(getUserNameOption);
      const userNameObj = JSON.parse(userLineAPIObj.body);
      const userName = userNameObj.displayName;

      // Slack通知
      const slackMessage = {
        "text"       : "[ LINE通知 ] " + message,
        "channel"    : "Cxxxxx",
        "username"   : userName,
        "icon_emoji" : ":warning:",
      }
      
      const slackSendOption = {
        method: 'POST',
        url: incomingWebhookUrl,
        encoding: null,
        headers: {
          "Content-type": "application/json",
        },
        json: slackMessage
      };
      const slackResult = await callAPI(slackSendOption);

      // LINE返信
      const replyText = (slackResult.statusCode === 200) ? "Slackに通知しました" : "Slack連携に失敗しました";
      const lineMessage = {
        "type" : "text",
        "text" : replyText,
      }
      const lineReplyOption = {
        method : "POST",
        url : lineReplyApiUrl,
        headers : {
          "Authorization" : "Bearer " + bearer,
          "Content-Type"  : "application/json",
        },
        json : {
          messages :[ 
            lineMessage
          ],
          replyToken : replyToken,
        }
      }
      await callAPI(lineReplyOption);
      return true;
    }
    return true
  }
}

const callAPI = (httpOption) => {
  return new Promise((resolve,reject) => {
    const request = require( 'request' );
    request( httpOption, ( err, res, buf ) => {
      if( err ){
        reject( { status: false, error: err } );
      }else{
        resolve(res);
      }
    });
  })
}

exports.main = main;
```
こんな感じで、`勤怠連絡`がメッセージに含まれる場合だけ、反応していることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/458c82f2-3137-9c77-6380-101a6bee1919.png)
Slackのチャンネルを確認すると、通知が飛んでいることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/cd3726e7-0056-4c19-a2f0-84295baf461c.png)

# グループトークへの対応
目的通りに動いたことを確認しましたが、いざ、この勤怠連絡してくれるBotをグループチャットに招待すると、動きません。
原因は、1対1の友達の状態でトーク相手のユーザー名を取得するAPIと、グループトークで投稿したユーザー名を取得するAPIが異なるためです。
全て記述すると長いので、部分的に以下のコードを追加するか書き換えてください。

```Javascript:action.js
// LINE情報取得
const replyToken = params.events[0].replyToken;
const userId = params.events[0].source.userId;
const groupId = params.events[0].source.groupId; // 追加：グループトークの場合はグループIDが取得できます。

// LINE ユーザー名取得(上は1対1のトーク用、下はグループトーク用)
//const getUserNameUrl = "https://api.line.me/v2/bot/profile/" + userId;
const getUserNameUrl = "https://api.line.me/v2/bot/group/" + groupId + "/member/" + userId;
```
グループトークの場合は、グループIDが取得できるので、それを用いてユーザー情報を取得しなければならない点が面倒なところです。三項演算子使えば良かったかな。。。

# さいごに
いかがでしたでしょうか？LINEはLINE developersに登録しさえすれば、簡単に連携できますし、Slackも通知だけであれば、Incoming Webhookの設定さえしてしまえば、投稿は簡単に行えるので、連携させると意外に便利なものが出来るかもしれません。
今回の記事は、コード成分が多めでしたが、難しいコードはそんなに無いので、試しに作ってみようという方もチャレンジできる範囲かと思います。
また、IBM Cloud Functionsを利用した記事も書いてみたいと思いますが、第3回までお付き合いありがとうございました。
　第1回：[IBM Cloud Functons 動かしてみた](https://qiita.com/katahiro/items/4b515c12e42e98e17eb7)
　第2回：[IBM Cloud FunctionsでNode.jsのパッケージを利用してみた](https://qiita.com/katahiro/items/1d483030527367338574)
　第3回：[IBM Cloud Functions でLINEとSlack連携させてみた](https://qiita.com/katahiro/items/298eac9c0fa875333d84)