Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

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

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

what3wordsとは

公式サイト

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

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

作りたいもの

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

手順

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

what3words API の使用方法

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

アカウント作成
20190620_01.jpg

アプリケーション作成
20190620_02.jpg

作成後、APIKEYが表示されるのでメモメモ



叩いてみる

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

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

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が返ってくるはず

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

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


LINE Location Messageの例

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

{
  "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
  "type": "message",
  "timestamp": 1462629479859,
  "source": {
    "type": "user",
    "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
  },
  "message": {
    "id": "325708",
    "type": "location",
    "title": "my location",
    "address": "〒150-0002 東京都渋谷区渋谷2丁目21−1",
    "latitude": 35.65910807942215,
    "longitude": 139.70372892916203
  }
}

message 内の latitude longitude でそのエリアの経度緯度が渡されているようですので、

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

こんな具合で,で結合。


画像検索

一番かんたんそうで手っ取り早かった フォト蔵API を選択。
今回はこだわりないので limit は1。

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);
  })


メッセージの複数同時PUSH

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

 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() で同時に返信します。

  // メッセージオブジェクトの初期設定
  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 でできることまとめ【送信編】


できたコード

クリックで展開
'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} 🦄`);

改善点

  • ワードがいちいち見慣れない単語が多いため、フォト蔵APIにおいて単語で画像検索してもヒットしないことがよくある・・・
  • ヒットしても、最初にヒットした画像を持ってきているだけなので「なんでこの画像?」がよくある・・・(日本語ならもっとマシな画像がヒットするだろうが what3words API は現時点で英語orフランス語のみ対応)

 → 一意の単語で「これぞ!」という画像を引っ張ってこれる仕組みを探したい(機械学習じみてきた)

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

 → 位置情報アクションを使って、いちいち左下の+アイコン経由させずに1オペで位置情報画面に遷移させてあげたい

protoout-studio
プロトアウトスタジオは日本初のプロトタイピング専門スクールです。プログラミングだけではなく、企画力と発信力を身に付けて”自分で課題を見つけて実装し、発信し続ける人”を育成しています。 圧倒的なアウトプット力を身に付けましょう。 学生募集中です。
https://protoout.studio
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away