どうも、シローです。
GoogleMapAPIでは「場所を入力すれば、緯度経度が取れる」
TwitterAPIでは「緯度経度があれば、周辺の投稿を取得できる」
(僕)「あれ?二つを組み合わせれば、場所を入力すればその周辺の投稿を取得できるんじゃね?w」
という誰でも思いつく素晴らしいことを思いついたので、ちょっとしたアプリを開発中です。
まだまだ、拡張予定ですが、とりあえずドヤしたい感が我慢できなかったので投稿しました。
デモ
本当はRadiusを変動して範囲指定をできるようにするつもりでした。
一見誰でも思いつくとは行っても、いざ行動に起こして作ろうとすると意外としんどかったです笑
構成
システムそのものは以下のような構成で動いてます。
当初はReact Appだけで動かせると思っていたのですが、nodeでTwitter APIを利用するモジュール(https://www.npmjs.com/package/twitter)がサーバサイドで動かすことを想定しているので、Expressをサーバとして立てて、React AppからサーバサイドのAPIを経由してTwitter APIにアクセスするようにしました。
React AppとExpressで公開しているポートが違うので、React Appに備わっているProxy MiddleWareで/api/**から始まるアクセスをhttp://localhost:5000/api/**に変換するようにしています。
ロジック解説
ソースの全部を載せるのはしんどいので、コアな部分だけ
- GoogleMapAPIで場所名を緯度経度に変換する
- TwitterAPIで緯度経度からツイートを取得する
を解説します。
検索ボタン押されたときの処理フロー
上の動画のように「Search」を押したときのフロートして
- 場所名から緯度経度に変換
- 緯度経度に紐づくツイートを取得
というふうになっています
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 緯度経度周辺の投稿を集める async searchPosts(e) { e.preventDefault(); if (!window.google) { return; } await this.geocodeLocation(); const { statuses } = await this.twitterGeocodeSearch(); const tweets = statuses; this.setState({ result: { ...this.state.result, tweets } }); } |
GoogleMapAPIで場所名を緯度経度に変換
前回の記事(https://shiro-secret-base.com/?p=738)でも触れましたが、こちらのモジュール(https://www.npmjs.com/package/@react-google-maps/api)をインストールします。
で、実際に緯度経度に変換している部分はこちら
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// 緯度経度をGeocode APIにより取得する geocodeLocation() { const geocoder = new window.google.maps.Geocoder(); return new Promise(resolve => { geocoder.geocode({ address: this.state.form.locationName }, (results, status) => { if (status === 'OK') { const locationInfo = results[0]; const lat = locationInfo.geometry.location.lat(); const lng = locationInfo.geometry.location.lng(); const center = { lat, lng }; let map = Object.assign({}, this.state.result.map); map.center = center; this.setState({ result: { ...this.state.result, map } }); } resolve(); }); }); } |
最終的に取得した緯度経度の情報をsetState
でステート更新してます。
この処理を実行した後で、次のツイート検索APIを実行します。
TwitterAPIで緯度経度からツイートを取得
geocodeLocation
の実行が完了したら、ステートに緯度経度が保存されているのでそれをサーバAPIを介してツイートを取得します。
なので、フロントからサーバにAPIでアクセスする部分とサーバからTwitter APIでツイートを取得してフロントに返す部分が必要になります。
フロントからサーバへのアクセスする部分はこちら、
1 2 3 4 5 6 7 8 9 10 11 |
// 緯度経度に紐づくTwitterの投稿を取得する async twitterGeocodeSearch() { const mapCenter = this.state.result.map.center; const { data } = await this.searchTweet(mapCenter); return data; } async searchTweet (mapCenter) { const query = `${mapCenter.lat},${mapCenter.lng},0.5km`; const response = await axios.get(`/api/twitter/search?geocode=${query}`); return response; } |
緯度経度をクエリパラメータにして渡していますね、今回は範囲は500mで固定にしました。
サーバからTwiterAPIにアクセスする部分はこちら、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
const express = require('express'); const Twitter = require('twitter'); require('dotenv').config(); const router = express.Router(); const twitterClient = new Twitter({ bearer_token: process.env.TWITTER_BEARER_TOKEN, }); const searchTweet = async (geocode) => { const options = { result_type: 'recent', geocode, }; const result = await twitterClient.get('search/tweets', options); return result; }; /* GET home page. */ router.get('/search', async (req, res) => { const { geocode } = req.query; res.json(await searchTweet(geocode)); }); module.exports = router; |
TwitterAPIに利用したモジュールはこちら(https://www.npmjs.com/package/twitter)
緯度経度を利用してツイートを取得できるTwitterのAPIのバージョンは1.1みたいでした。(新バージョンではない)
そして、リファレンスはこちら(https://developer.twitter.com/en/docs/twitter-api/v1/tweets/search/api-reference/get-search-tweets)
TwitterAPIの利用方法については省略します、デタラメなことを申請内容に書いても弾かれるので注意しましょう^^
APIが利用できれば、BearerTokenを発行し、.env
のTWITTER_BEARER_TOKEN
に設定します。
実行してフロントに返している結果はこんな感じになりました
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
{ statuses: [ { created_at: 'Mon Mar 29 14:14:58 +0000 2021', id: 1376538375893741600, id_str: '1376538375893741570', text: '今日のお昼ご飯はこちら!\n' + 'バンコクスマイルにて\n' + '鶏肉のグリーンカレーを食べました〜\n' + '美味しいなー\n' + 'ごちそうさまでした♪\n' + '\n' + '#お昼ごはん #lunch #バンコクスマイル #タイ料理 #グリーンカレー #美味 #delicious… https://t.co/yAL89cufZB', truncated: true, entities: [Object], metadata: [Object], source: '<a href="http://instagram.com" rel="nofollow">Instagram</a>', in_reply_to_status_id: null, in_reply_to_status_id_str: null, in_reply_to_user_id: null, in_reply_to_user_id_str: null, in_reply_to_screen_name: null, user: [Object], geo: [Object], coordinates: [Object], place: [Object], contributors: null, is_quote_status: false, retweet_count: 0, favorite_count: 0, favorited: false, retweeted: false, possibly_sensitive: false, lang: 'ja' }, { created_at: 'Mon Mar 29 13:56:30 +0000 2021', id: 1376533729078747100, id_str: '1376533729078747137', text: '最上階レストランで夜景とか地上で桜とか。普段とは違う趣きの景色で感動。但し服装は間違えた😣 https://t.co/xfdyNxkvnP', truncated: false, entities: [Object], extended_entities: [Object], metadata: [Object], source: '<a href="http://twitter.com/download/iphone" rel="nofollow">Twitter for iPhone</a>', in_reply_to_status_id: null, in_reply_to_status_id_str: null, in_reply_to_user_id: null, in_reply_to_user_id_str: null, in_reply_to_screen_name: null, user: [Object], geo: null, coordinates: null, place: [Object], contributors: null, is_quote_status: false, retweet_count: 0, favorite_count: 0, favorited: false, retweeted: false, possibly_sensitive: true, lang: 'ja' }, . . . } |
後はフロントで上手く必要なデータを抽出して投稿を表示するだけです。
おしまい