Node.js で what3words を返す LINE Bot を作る

# はじめに

Node.jsでLINE Botを作成する練習をします。

基本的に [菅原のびすけ (@n0bisuke) ](https://qiita.com/n0bisuke) さんの以下の記事を参考にしています！
[1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest](https://qiita.com/n0bisuke/items/ceaa09ef8898bee8369d)


# what3wordsとは
[公式サイト](https://what3words.com)

> what3words（ワットスリーワーズ）は、3メートルの解像度で場所を伝達するためのジオコーディングシステムである。what3wordsでは、地理的座標を3つの単語で符号化する。例えば、自由の女神像の持つ松明の位置は、"toned.melt.ship"の3語で表している。従来のほかの位置エンコードシステムとの違いは、長い文字(たとえば住所)や数字(たとえば経緯度)ではなく、3つの単語で簡単に表される点にある。

3mx3m単位で表現されるので、例えば待ち合わせで「〇〇駅の東口の自動販売機の右側」の粒度で相手に情報を連携できます。

## 作りたいもの

LINE Bot に位置情報共有を送信すると、waht3words API に経度・緯度を渡して該当の３ワードをレスポンスします。
３ワードだけだと寂しいので、３ワードそれぞれから推測される画像を画像カルーセル形式で返信してあげます。
（モンスターファームやオトッペなどなど・・・身近なものからモンスターを作り出すみたいな既存サービスのように
場所に愛着をもってもらえるかな～と思って。。）


## 手順

LINE Botアカウントの作成やチャネル開設、オウム返しBotの作成方法は元記事を参照！
[1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest](https://qiita.com/n0bisuke/items/ceaa09ef8898bee8369d)

#### what3words API の使用方法

まず、公式でアカウント作成・アプリケーション作成・ライセンスキー発行を行いましょう。
https://accounts.what3words.com/register/

**アカウント作成**
![20190620_01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178043/3ebda33a-bb72-1a74-bd01-c763f46051dc.jpeg)

**アプリケーション作成**
![20190620_02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178043/e8c8cb9b-254f-638c-0e96-209e73ae12d6.jpeg)

**作成後、APIKEYが表示されるのでメモメモ**
<br><br><br>
#### 叩いてみる

経度緯度(Coordinate)⇒３ワードの場合は https://api.what3words.com/v3/convert-to-3wa を使用します。
クエリパラメータとして`coordinates`で経度緯度を、`key`でAPIKEYを渡してあげます。
(axiosを使用します)

経度緯度は`,`繋ぎで(`35.000000,139.000000`)で設定します。

```javascript:
const axios = require('axios');

axios
    .get('https://api.what3words.com/v3/convert-to-3wa', {
      params: {
        coordinates: '35.000000,139.000000',  //カンマ繋ぎ
        key: 'APIKEY',
        language: 'en',
        format: 'json'
      }
    })
    .then(async function(res) {
        console.log(res.data);
    });
```
こんなJSONが返ってくるはず

```json
{
  country: 'JP',
  square: {
    southwest: { lng: 139.000000, lat: 35.000000 },
    northeast: { lng: 139.000000, lat: 35.000000 }
  },
  nearestPlace: 'Tokyo, Tōkyō',
  coordinates: { lng: 139.000000, lat: 35.000000 },
  words: 'flanks.picture.ledge',
  language: 'en',
  map: 'https://w3w.co/flanks.picture.ledge'
}
```

`words`に該当の３ワードが`.`繋ぎでレスポンスされています。
今回の場合は `///flanks・picture・ledge` ですね。
公式サイトで見てみると日本語のワードが出てきますが、APIは現時点で日本語対応していないようです。

（今気づきましたが、同じエリアでも言語ごとに全く異なるワードが割り振られるのですね。。。
　日本語の場合の同エリアは、`///るんるん・じむふく・ちのう` になりました。）

<br>

#### LINE Location Messageの例

次にLINEで位置情報共有した際にどんなリクエストが飛んでいるか見てみます。

```json
{
  "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
  "type": "message",
  "timestamp": 1462629479859,
  "source": {
    "type": "user",
    "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
  },
  "message": {
    "id": "325708",
    "type": "location",
    "title": "my location",
    "address": "〒150-0002 東京都渋谷区渋谷２丁目２１−１",
    "latitude": 35.65910807942215,
    "longitude": 139.70372892916203
  }
}
```
`message` 内の `latitude` `longitude` でそのエリアの経度緯度が渡されているようですので、

```javascript
  const lat = event.message.latitude;
  const lng = event.message.longitude;
  const coordinates = lat + ',' + lng;
```

こんな具合で`,`で結合。

<br>

#### 画像検索

一番かんたんそうで手っ取り早かった [フォト蔵API](http://photozou.jp/basic/api) を選択。
今回はこだわりないので `limit` は１。 

```javascript
axios
  .get('http://api.photozou.jp/rest/search_public.json', {
    params: {
      keyword: targetWord,
      limit: '1'
    }
  })
  .then(function(res) {
    const targetImageUrl = res.data.info.photo[0].image_url;
    console.log(targetImageUrl);
  })
```

<br>

#### メッセージの複数同時PUSH

任意のタイミングでBotからメッセージを送信させるには `pushMessage(userId, message)` を使います。
また、

```javascript
 const messages = [
    { type: 'text', text: 'ひとことめ' },
    { type: 'text', text: 'ふたことめ' }
  ];
 client.pushMessage(userId, messages);
```
メッセージオブジェクトを配列で返すと一度のプッシュで複数メッセージを送信できます。

今回は、
  1. `その場所の 3 ワードは...`
  2. `「hoge」「fuga」「huwa」です！`
  3. `（ヒットした画像カルーセル）`
  4. `（what3wordsへのURL）`
をメッセージとして返したいです。

「1.」はリクエスト契機で `replyMessage(replyToken, message)` ですぐさま返信・・・
「2.〜4.」は配列にして一度の `pushMessage()` で同時に返信します。

```javascript
  // メッセージオブジェクトの初期設定
  let replyMessageObject = [
    {
      type: 'text',
      text: `` //後から代入
    },
    {
      type: 'template',
      altText: 'Image carousel alt text',
      template: {
        type: 'image_carousel',
        columns: [
          {
            imageUrl: '', //後から代入
            action: {
              label: '', //後から代入
              type: 'message',
              text: 'へー' //画像クリックでこの文字列が自メッセージとして送信される
            }
          },
          {
            imageUrl: '', //後から代入
            action: {
              label: '', //後から代入
              type: 'message',
              text: 'ふーん' //空だとエラー
            }
          },
          {
            imageUrl: '', //後から代入
            action: {
              label: '', //後から代入
              type: 'message',
              text: 'ほー' //ほー
            }
          }
        ]
      }
    },
    { type: 'text', text: '' }
  ];
```


LINE Message の種類はこの記事がすごく理解しやすかったです！
[LINE Messaging API でできることまとめ【送信編】](https://qiita.com/kakakaori830/items/52e52d969800de61ce28)

<br>

### できたコード

<details><summary>クリックで展開</summary><div>

```javascript
'use strict';

const express = require('express');
const line = require('@line/bot-sdk');
const lineConfig = {
  channelSecret: '__channelSecret__',
  channelAccessToken: '__channelAccessToken__'
};
const client = new line.Client(lineConfig);
const PORT = process.env.PORT || 3000;

const axios = require('axios');

const app = express();

app.get('/', (req, res) => res.send('LINE BOT'));

app.post('/webhook', line.middleware(lineConfig), (req, res) => {
  console.log(req.body.events);
  Promise.all(req.body.events.map(handleEvent)).then(result =>
    res.json(result)
  );
});

function handleEvent(event) {
  //event.message.typeがlocation以外なら警告
  if (event.type !== 'message' || event.message.type !== 'location') {
    if (
      event.message.text !== 'へー' &&
      event.message.text !== 'ふーん' &&
      event.message.text !== 'ほー'
    ) {
      return client.replyMessage(event.replyToken, {
        type: 'text',
        text: '位置情報を送信してください！ \n入力欄の左側からできますよ😄'
      });
    } else {
      return client.replyMessage(event.replyToken, {
        type: 'text',
        text: '🍣'
      });
    }
  }

  const userId = event.source.userId; // 送信したユーザーID

  const lat = event.message.latitude;
  const lng = event.message.longitude;

  // w3w用のコンフィグにLocation Messageの経度緯度を設定
  const coordinates = lat + ',' + lng;

  // w3w API 実行
  getW3W(userId, coordinates);

  return client.replyMessage(event.replyToken, {
    type: 'text',
    text: 'その場所の 3 ワードは...'
  });
}

const getW3W = async (userId, coordinates) => {
  const targetCoord = coordinates;

  // メッセージオブジェクトの初期設定
  let replyMessageObject = [
    {type: 'text',text: ``},
    {
      type: 'template',
      altText: 'Image carousel alt text',
      template: {
        type: 'image_carousel',
        columns: [
          {imageUrl: '',action: {label: '',type: 'message',text: 'へー'}},
          {imageUrl: '',action: {label: '',type: 'message',text: 'ふーん'}},
          {imageUrl: '',action: {label: '',type: 'message',text: 'ほー'}}
        ]
      }
    },
    { type: 'text', text: '' }
  ];

  const res = await axios
    .get('https://api.what3words.com/v3/convert-to-3wa', {
      params: {
        coordinates: targetCoord,
        key: 'YH68ALQA',
        language: 'en',
        format: 'json'
      }
    })
    .then(async function(res) {
      const item = res.data;
      const words = item.words;
      const what3words = words.split('.');
      const map = item.map;
      console.log('words:', words);
      console.log('what3words:', what3words);
      console.log('map:', map);
      replyMessageObject[0].text = `「${what3words[0]}」「${what3words[1]}」「${
        what3words[2]
      }」です！`;
      replyMessageObject[1].template.columns[0].action.label = what3words[0];
      replyMessageObject[1].template.columns[1].action.label = what3words[1];
      replyMessageObject[1].template.columns[2].action.label = what3words[2];
      replyMessageObject[2].text = map;

      for (let i = 0; i < what3words.length; i++) {
        replyMessageObject[1].template.columns[i].imageUrl = await getPhotoZo(
          what3words[i]
        );
        console.log(
          'replyMessageObject[1].template.columns[i].imageUrl:',
          replyMessageObject[1].template.columns[i].imageUrl
        );
      }

      await client.pushMessage(userId, replyMessageObject);
    });
};

const getPhotoZo = async function(targetWord) {
  let targetImageUrl = '';
  targetImageUrl = await axios
    .get('http://api.photozou.jp/rest/search_public.json', {
      params: {
        keyword: targetWord,
        limit: '1'
      }
    })
    .then(function(res) {
      // 検索結果が配列かどうか判定
      if (res.data.info.photo instanceof Array) {
        targetImageUrl = res.data.info.photo[0].image_url;
      } else {
        // 配列じゃない場合は[無]画像
        targetImageUrl =
          'https://1.bp.blogspot.com/-3wsYvUnKhNE/Wc8f-o6JCrI/AAAAAAABHJw/xo60FyvnUp4DKZ53GOcCAXn1KxcYBbyygCLcBGAs/s800/text_mu.jpg';
      }
      targetImageUrl = targetImageUrl.replace('http:', 'https:');
      return targetImageUrl;
    })
    .catch(function(err) {
      targetImageUrl =
        'https://placehold.jp/3d4070/ffffff/150x150.jpg?text=error';
      console.log(err);
    });
  return targetImageUrl;
};

process.env.NOW_REGION ? (module.exports = app) : app.listen(PORT);
console.log(`Server running at ${PORT} 🦄`);

```

</div></details>

## 改善点

 * ワードがいちいち見慣れない単語が多いため、フォト蔵APIにおいて単語で画像検索してもヒットしないことがよくある・・・
 * ヒットしても、最初にヒットした画像を持ってきているだけなので「なんでこの画像？」がよくある・・・（日本語ならもっとマシな画像がヒットするだろうが what3words API は現時点で英語orフランス語のみ対応）
 
　→　一意の単語で「これぞ！」という画像を引っ張ってこれる仕組みを探したい（機械学習じみてきた）

 * LINEの位置情報共有に計４タップ必要・・・


　→　[位置情報アクション](https://developers.line.biz/ja/docs/messaging-api/actions/#postback-action)を使って、いちいち左下の＋アイコン経由させずに１オペで位置情報画面に遷移させてあげたい