Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9055

【個人開発/LINE Messaging API】Node.js, TypeScriptで現在地から美味しいお店を探すアプリを作ってみた(②)

$
0
0
LINE Messaging APIを使って現在地から美味しいお店を探すアプリを作ってみました。 完成形としては以下の通りです。 アーキテクチャ 今回はサーバーレスアーキテクチャの根幹の機能である、「AWS Lambda」, 「Amazon API Gateway」, 「Amazon DynamoDB」の3つを使用してアプリを作成します。 また、プロビジョニングやデプロイに関してはAWS SAM(Serverless Application Model)を使用します。 対象読者 ・ Node.jsを勉強中の方 ・ TypeScriptを勉強中の方 ・ インフラ初心者の方 ・ ポートフォリオのデプロイ先をどうするか迷っている方 作成の難易度は低めです。 理由は、必要なパッケージも少ないため要件が複雑ではないからです。 また、DynamoDBの操作は、CRUD(作成・読み込み・更新・削除)のうち、C・R・Uの3つを使用するので、初学者の踏み台アプリとして優秀かと思います。 記事 今回は2つの記事に分かれています。 お気に入り店の登録や解除などを今回の記事で行っています。 お店の検索を行うところまでを前の記事で行っています。 前回の記事をやっていないと今回の記事は全く理解できないのでこちらからご確認ください。 Github 完成形のコードは以下となります。 アプリQR こちらから触ってみてください。 アプリの機能 今回は3つの機能を足していきます。 お気に入り登録 クライアント LINE Messaging API(バックエンド) ①メッセージを編集し、「行きつけ」ボタンを追加する ②「行きつけ」をタップする ③DynamoDBを作成する ④ポストバックのデータを元にDynamoDBに登録を行う お気に入り店を探す クライアント LINE Messaging API(バックエンド) ①「行きつけ」をタップする ②user_idを元にDynamoDB から検索を行う ③FlexMessageを作成する ④お店の情報をFlexMessageで送る お気に入り店の解除 クライアント LINE Messaging API(バックエンド) ①「行きつけを解除」をタップする ②user_idとtimestampを元にDynamoDBからデータを削除する ハンズオン! お気に入り登録を行う 機能 これだけじゃイメージがつきにくいと思うので完成図を先に見せます。 ①メッセージを編集し、「行きつけ」ボタンを追加する 「行きつけ」をタップすることで、お店の情報を渡したいのでポストバックアクションを使用します。 こちらを使うことで、dataプロパティの値を受け取ることができます。 普通にメッセージとして送ってもいいのですが、送られる返信がお店の情報になってしまいます。 あまりよろしくないので、採用を見送りました。 ということでやっていきましょう。 今回は前回の記事で作成したCreateFlexMessage.tsに追加していきます。 api/src/Common/TemplateMessage/Gourmet/CreateFlexMessage.ts // Load the package import { FlexMessage, FlexCarousel, FlexBubble } from '@line/bot-sdk'; // Load the module import { sortRatingGourmetArray } from './SortRatingGourmetArray'; // types import { Gourmet, RatingGourmetArray } from './types/CreateFlexMessage.type'; export const createFlexMessage = async ( user_id: string | undefined, googleMapApi: string ): Promise<FlexMessage | undefined> => { return new Promise(async (resolve, reject) => { try { // modules sortRatingGourmetArray const ratingGourmetArray: RatingGourmetArray = await sortRatingGourmetArray( user_id, googleMapApi ); // FlexMessage const FlexMessageContents: FlexBubble[] = await ratingGourmetArray.map((gourmet: Gourmet) => { // Create a URL for a store photo const photoURL = `https://maps.googleapis.com/maps/api/place/photo?maxwidth=400&photoreference=${gourmet.photo_reference}&key=${googleMapApi}`; // Create a URL for the store details const encodeURI = encodeURIComponent(`${gourmet.name} ${gourmet.vicinity}`); const storeDetailsURL = `https://maps.google.co.jp/maps?q=${encodeURI}&z=15&iwloc=A`; // Create a URL for store routing information const storeRoutingURL = `https://www.google.com/maps/dir/?api=1&destination=${gourmet.geometry_location_lat},${gourmet.geometry_location_lng}`; const flexBubble: FlexBubble = { type: 'bubble', hero: { type: 'image', url: photoURL, size: 'full', aspectMode: 'cover', aspectRatio: '20:13', }, body: { type: 'box', layout: 'vertical', contents: [ { type: 'text', text: gourmet.name, size: 'xl', weight: 'bold', }, { type: 'box', layout: 'baseline', contents: [ { type: 'icon', url: 'https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png', size: 'sm', }, { type: 'text', text: `${gourmet.rating}`, size: 'sm', margin: 'md', color: '#999999', }, ], margin: 'md', }, ], }, footer: { type: 'box', layout: 'vertical', contents: [ { type: 'button', action: { type: 'uri', label: '店舗詳細', uri: storeDetailsURL, }, }, { type: 'button', action: { type: 'uri', label: '店舗案内', uri: storeRoutingURL, }, }, + { + type: 'button', + action: { + type: 'postback', + label: '行きつけ', + data: `lat=${gourmet.geometry_location_lat}&lng=${gourmet.geometry_location_lng}&name=${gourmet.name}&photo=${gourmet.photo_reference}&rating=${gourmet.rating}&vicinity=${gourmet.vicinity}`, + displayText: '行きつけにする', + }, + }, ], spacing: 'sm', }, }; return flexBubble; }); const flexContainer: FlexCarousel = { type: 'carousel', contents: FlexMessageContents, }; const flexMessage: FlexMessage = { type: 'flex', altText: '近隣の美味しいお店10店ご紹介', contents: flexContainer, }; resolve(flexMessage); } catch (err) { reject(err); } }); }; ②「行きつけ」をタップする こちらはクライアント側での操作なのでやることはありません。 ③DynamoDBを作成する 前回の記事では、SAMテンプレートでDynamoDBを作成したのですが、今回は手動で作成します。 DBは以下のような値を持つレコードを作成していきます。 PK SK K K K K K user_id timestamp photo_url name rating store_details_url store_routing_url ユーザー ID タイムスタンプ 店舗の写真 店舗の名前 店舗の評価 店舗詳細 店舗案内 ソートキーを使う場合どのようにSAMを使うのかの記載が見つからなかったので手動とします。(SAMテンプレートでのやり方を知っている方がいましたらお教えいただけますと幸いです。) DynamoDBを作ったことがない人もいると思うので、一応画像で説明します。 名前は何でもいいです。 一応自分は、Gourmets_Favoriteで作成しています。 先に作成しているのでエラーメッセージ出てますが気にしないでください。 ④ポストバックのデータを元にDynamoDBに登録を行う まずは関数を呼び出しているindex.tsから記載していきます。 api/src/index.ts // Load the package import { ClientConfig, Client, WebhookEvent } from '@line/bot-sdk'; import aws from 'aws-sdk'; // Load the module // TemplateMessage import { yourLocationTemplate } from './Common/TemplateMessage/YourLocation'; import { errorTemplate } from './Common/TemplateMessage/Error'; import { isCarTemplate } from './Common/TemplateMessage/IsCar'; import { createFlexMessage } from './Common/TemplateMessage/Gourmet/CreateFlexMessage'; // Database import { putLocation } from './Common/Database/PutLocation'; import { updateIsCar } from './Common/Database/UpdateIsCar'; + import { putFavorite } from './Common/Database/PutFavorite'; // SSM const ssm = new aws.SSM(); const LINE_GOURMET_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_GOURMET_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const LINE_GOURMET_CHANNEL_SECRET = { Name: 'LINE_GOURMET_CHANNEL_SECRET', WithDecryption: false, }; const LINE_GOURMET_GOOGLE_MAP_API = { Name: 'LINE_GOURMET_GOOGLE_MAP_API', WithDecryption: false, }; exports.handler = async (event: any, context: any) => { // Retrieving values in the SSM parameter store const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_GOURMET_CHANNEL_ACCESS_TOKEN) .promise(); const CHANNEL_SECRET: any = await ssm.getParameter(LINE_GOURMET_CHANNEL_SECRET).promise(); const GOOGLE_MAP_API: any = await ssm.getParameter(LINE_GOURMET_GOOGLE_MAP_API).promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; const channelSecret: string = CHANNEL_SECRET.Parameter.Value; const googleMapApi: string = GOOGLE_MAP_API.Parameter.Value; // Create a client using the SSM parameter store const clientConfig: ClientConfig = { channelAccessToken: channelAccessToken, channelSecret: channelSecret, }; const client = new Client(clientConfig); // body const body: any = JSON.parse(event.body); const response: WebhookEvent = body.events[0]; // action try { await actionLocationOrError(client, response); await actionIsCar(client, response); await actionFlexMessage(client, response, googleMapApi); + await actionPutFavoriteShop(response, googleMapApi); } catch (err) { console.log(err); } }; // 位置情報もしくはエラーメッセージを送る const actionLocationOrError = async (client: Client, event: WebhookEvent): Promise<void> => { try { // If the message is different from the target, returned if (event.type !== 'message' || event.message.type !== 'text') { return; } // Retrieve the required items from the event const replyToken = event.replyToken; const text = event.message.text; // modules const yourLocation = await yourLocationTemplate(); const error = await errorTemplate(); // Perform a conditional branch if (text === 'お店を探す') { await client.replyMessage(replyToken, yourLocation); } else if (text === '車' || text === '徒歩') { return; } else if (text === '行きつけのお店') { return; } else { await client.replyMessage(replyToken, error); } } catch (err) { console.log(err); } }; // 移動手段の「車もしくは徒歩」かを尋ねるメッセージを送る const actionIsCar = async (client: Client, event: WebhookEvent): Promise<void> => { try { // If the message is different from the target, returned if (event.type !== 'message' || event.message.type !== 'location') { return; } // Retrieve the required items from the event const replyToken = event.replyToken; const userId = event.source.userId; const latitude: string = String(event.message.latitude); const longitude: string = String(event.message.longitude); // Register userId, latitude, and longitude in DynamoDB await putLocation(userId, latitude, longitude); // modules const isCar = await isCarTemplate(); // Send a two-choice question await client.replyMessage(replyToken, isCar); } catch (err) { console.log(err); } }; // 上記の選択を経て、おすすめのお店をFlex Messageにして送る const actionFlexMessage = async (client: Client, event: WebhookEvent, googleMapApi: string) => { try { // If the message is different from the target, returned if (event.type !== 'message' || event.message.type !== 'text') { return; } // Retrieve the required items from the event const replyToken = event.replyToken; const userId = event.source.userId; const isCar = event.message.text; // Perform a conditional branch if (isCar === '車' || isCar === '徒歩') { // Register userId, isCar in DynamoDB await updateIsCar(userId, isCar); const flexMessage = await createFlexMessage(userId, googleMapApi); if (flexMessage === undefined) { return; } await client.replyMessage(replyToken, flexMessage); } else { return; } } catch (err) { console.log(err); } }; + // FlexMessageの「行きつけ」をタップしたらそのお店が登録される + const actionPutFavoriteShop = async (event: WebhookEvent, googleMapApi: + string) => { + try { + // If the message is different from the target, returned + if (event.type !== 'postback') { + return; + } + + // Retrieve the required items from the event + const data = event.postback.data; + const timestamp = event.timestamp; + const userId = event.source.userId; + + // conditional branching + const isFavorite = data.indexOf('timestamp'); + if (isFavorite === -1) { + // Register data, userId in DynamoDB + await putFavorite(data, timestamp, userId, googleMapApi); + } + } catch (err) { + console.log(err); + } + }; では、DynamoDBにデータを追加するコードを書いていきましょう。 データの追加はputを使用します。 また、次にポストバックのデータの使用方法に関してです。 { "type":"postback", "label":"Buy", + "data":"action=buy&itemid=111", "text":"Buy" } データはこのように渡されます。 この値をどのように取得するかお分かりでしょうか? JavaScriptに慣れている方であればすぐにお分かりでしょうね! 指定した区切り文字で分割して文字列の配列にしましょう。 ということで使うのは、splitですね。 ということでやっていきましょう。 api/src/Common/Database/PutFavorite.ts // Load the package import aws from 'aws-sdk'; // Create DynamoDB document client const docClient = new aws.DynamoDB.DocumentClient(); export const putFavorite = ( data: string, timestamp: number, userId: string | undefined, googleMapApi: string ) => { return new Promise((resolve, reject) => { // data const dataArray = data.split('&'); const lat = dataArray[0].split('=')[1]; const lng = dataArray[1].split('=')[1]; const name = dataArray[2].split('=')[1]; const photo = dataArray[3].split('=')[1]; const rating = dataArray[4].split('=')[1]; const vicinity = dataArray[5].split('=')[1]; // Create a URL for a store photo const photoURL = `https://maps.googleapis.com/maps/api/place/photo?maxwidth=400&photoreference=${photo}&key=${googleMapApi}`; // Create a URL for the store details const encodeURI = encodeURIComponent(`${name} ${vicinity}`); const storeDetailsURL = `https://maps.google.co.jp/maps?q=${encodeURI}&z=15&iwloc=A`; // Create a URL for store routing information const storeRoutingURL = `https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}`; const params = { Item: { user_id: userId, timestamp: timestamp, photo_url: photoURL, name: name, rating: rating, store_details_url: storeDetailsURL, store_routing_url: storeRoutingURL, }, ReturnConsumedCapacity: 'TOTAL', TableName: 'Gourmets_Favorite', }; docClient.put(params, (err, data) => { if (err) { reject(err); } else { resolve(data); } }); }); }; これで完了です。 それでは次に、お気に入りのお店を探しましょう。 お気に入り店を探す 機能 こちらも先にどのような機能かお見せします。 「行きつけ」をタップしたら、お気に入り登録したお店の一覧が表示されます。 ①「行きつけ」をタップする こちらはクライアント側の操作なので特にすることはありません。 ②user_idを元にDynamoDBから検索を行う DynamoDBからお気に入りのお店の情報を取得しましょう。 今回は複数取得する可能性が高いのでqueryを使用します。 ということでやっていきましょう。 api/src/Common/TemplateMessage/Favorite/QueryDatabaseInfo.ts // Load the package import aws from 'aws-sdk'; // Create DynamoDB document client const docClient = new aws.DynamoDB.DocumentClient(); export const queryDatabaseInfo = async (userId: string | undefined) => { return new Promise((resolve, reject) => { const params = { TableName: 'Gourmets_Favorite', ExpressionAttributeNames: { '#u': 'user_id' }, ExpressionAttributeValues: { ':val': userId }, KeyConditionExpression: '#u = :val', }; docClient.query(params, (err, data) => { if (err) { reject(err); } else { resolve(data); } }); }); }; ③FlexMessageを作成する DynamoDBから取得した値を使用してFlexMessageを作成していきましょう。 api/src/Common/TemplateMessage/Favorite/MakeFlexMessage.ts // Load the package import { FlexMessage, FlexCarousel, FlexBubble } from '@line/bot-sdk'; // Load the module import { queryDatabaseInfo } from './QueryDatabaseInfo'; // types import { Item, QueryItem } from './types/MakeFlexMessage.type'; export const makeFlexMessage = async (userId: string | undefined): Promise<FlexMessage> => { return new Promise(async (resolve, reject) => { try { // modules queryDatabaseInfo const query: any = await queryDatabaseInfo(userId); const queryItem: QueryItem = query.Items; // FlexMessage const FlexMessageContents: FlexBubble[] = await queryItem.map((item: Item) => { const flexBubble: FlexBubble = { type: 'bubble', hero: { type: 'image', url: item.photo_url, size: 'full', aspectMode: 'cover', aspectRatio: '20:13', }, body: { type: 'box', layout: 'vertical', contents: [ { type: 'text', text: item.name, size: 'xl', weight: 'bold', }, { type: 'box', layout: 'baseline', contents: [ { type: 'icon', url: 'https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png', size: 'sm', }, { type: 'text', text: `${item.rating}`, size: 'sm', margin: 'md', color: '#999999', }, ], margin: 'md', }, ], }, footer: { type: 'box', layout: 'vertical', contents: [ { type: 'button', action: { type: 'uri', label: '店舗詳細', uri: item.store_details_url, }, }, { type: 'button', action: { type: 'uri', label: '店舗案内', uri: item.store_routing_url, }, }, { type: 'button', action: { type: 'postback', label: '行きつけを解除', data: `timestamp=${item.timestamp}`, displayText: '行きつけを解除する', }, }, ], spacing: 'sm', }, }; return flexBubble; }); const flexContainer: FlexCarousel = { type: 'carousel', contents: FlexMessageContents, }; const flexMessage: FlexMessage = { type: 'flex', altText: 'お気に入りのお店', contents: flexContainer, }; resolve(flexMessage); } catch (err) { reject(err); } }); }; 独自の型定義があるのでファイルを作成しましょう。 api/src/Common/TemplateMessage/Favorite/types/MakeFlexMessage.type.ts export type Item = { user_id: string; photo_url: string; rating: string; timestamp: number; name: string; store_routing_url: string; store_details_url: string; }; export type QueryItem = Item[]; ④お店の情報をFlexMessageで送る 最後にFlexMessageで送信しましょう。 api/src/index.ts // Load the package import { ClientConfig, Client, WebhookEvent } from '@line/bot-sdk'; import aws from 'aws-sdk'; // Load the module // TemplateMessage import { yourLocationTemplate } from './Common/TemplateMessage/YourLocation'; import { errorTemplate } from './Common/TemplateMessage/Error'; import { isCarTemplate } from './Common/TemplateMessage/IsCar'; import { createFlexMessage } from './Common/TemplateMessage/Gourmet/CreateFlexMessage'; + import { makeFlexMessage } from './Common/TemplateMessage/Favorite/MakeFlexMessage'; // Database import { putLocation } from './Common/Database/PutLocation'; import { updateIsCar } from './Common/Database/UpdateIsCar'; import { putFavorite } from './Common/Database/PutFavorite'; // SSM const ssm = new aws.SSM(); const LINE_GOURMET_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_GOURMET_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const LINE_GOURMET_CHANNEL_SECRET = { Name: 'LINE_GOURMET_CHANNEL_SECRET', WithDecryption: false, }; const LINE_GOURMET_GOOGLE_MAP_API = { Name: 'LINE_GOURMET_GOOGLE_MAP_API', WithDecryption: false, }; exports.handler = async (event: any, context: any) => { // Retrieving values in the SSM parameter store const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_GOURMET_CHANNEL_ACCESS_TOKEN) .promise(); const CHANNEL_SECRET: any = await ssm.getParameter(LINE_GOURMET_CHANNEL_SECRET).promise(); const GOOGLE_MAP_API: any = await ssm.getParameter(LINE_GOURMET_GOOGLE_MAP_API).promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; const channelSecret: string = CHANNEL_SECRET.Parameter.Value; const googleMapApi: string = GOOGLE_MAP_API.Parameter.Value; // Create a client using the SSM parameter store const clientConfig: ClientConfig = { channelAccessToken: channelAccessToken, channelSecret: channelSecret, }; const client = new Client(clientConfig); // body const body: any = JSON.parse(event.body); const response: WebhookEvent = body.events[0]; // console.log(JSON.stringify(response)); // action try { await actionLocationOrError(client, response); await actionIsCar(client, response); await actionFlexMessage(client, response, googleMapApi); await actionPutFavoriteShop(response, googleMapApi); + await actionTapFavoriteShop(client, response); } catch (err) { console.log(err); } }; // 位置情報もしくはエラーメッセージを送る const actionLocationOrError = async (client: Client, event: WebhookEvent): Promise<void> => { try { // If the message is different from the target, returned if (event.type !== 'message' || event.message.type !== 'text') { return; } // Retrieve the required items from the event const replyToken = event.replyToken; const text = event.message.text; // modules const yourLocation = await yourLocationTemplate(); const error = await errorTemplate(); // Perform a conditional branch if (text === 'お店を探す') { await client.replyMessage(replyToken, yourLocation); } else if (text === '車' || text === '徒歩') { return; } else if (text === '行きつけのお店') { return; } else { await client.replyMessage(replyToken, error); } } catch (err) { console.log(err); } }; // 移動手段の「車もしくは徒歩」かを尋ねるメッセージを送る const actionIsCar = async (client: Client, event: WebhookEvent): Promise<void> => { try { // If the message is different from the target, returned if (event.type !== 'message' || event.message.type !== 'location') { return; } // Retrieve the required items from the event const replyToken = event.replyToken; const userId = event.source.userId; const latitude: string = String(event.message.latitude); const longitude: string = String(event.message.longitude); // Register userId, latitude, and longitude in DynamoDB await putLocation(userId, latitude, longitude); // modules const isCar = await isCarTemplate(); // Send a two-choice question await client.replyMessage(replyToken, isCar); } catch (err) { console.log(err); } }; // 上記の選択を経て、おすすめのお店をFlex Messageにして送る const actionFlexMessage = async (client: Client, event: WebhookEvent, googleMapApi: string) => { try { // If the message is different from the target, returned if (event.type !== 'message' || event.message.type !== 'text') { return; } // Retrieve the required items from the event const replyToken = event.replyToken; const userId = event.source.userId; const isCar = event.message.text; // Perform a conditional branch if (isCar === '車' || isCar === '徒歩') { // Register userId, isCar in DynamoDB await updateIsCar(userId, isCar); const flexMessage = await createFlexMessage(userId, googleMapApi); if (flexMessage === undefined) { return; } await client.replyMessage(replyToken, flexMessage); } else { return; } } catch (err) { console.log(err); } }; // FlexMessageの「行きつけ」をタップしたらそのお店が登録される const actionPutFavoriteShop = async (event: WebhookEvent, googleMapApi: string) => { try { // If the message is different from the target, returned if (event.type !== 'postback') { return; } // Retrieve the required items from the event const data = event.postback.data; const timestamp = event.timestamp; const userId = event.source.userId; // conditional branching const isFavorite = data.indexOf('timestamp'); if (isFavorite === -1) { // Register data, userId in DynamoDB await putFavorite(data, timestamp, userId, googleMapApi); } } catch (err) { console.log(err); } }; + // リッチメニューの「行きつけ」をタップしたらメッセージが送られる + const actionTapFavoriteShop = async (client: Client, event: WebhookEvent) => { + // If the message is different from the target, returned + if (event.type !== 'message' || event.message.type !== 'text') { + return; + } + + // Retrieve the required items from the event + const replyToken = event.replyToken; + const userId = event.source.userId; + const text = event.message.text; + + if (text === '行きつけのお店') { + const flexMessage = await makeFlexMessage(userId); + if (flexMessage === undefined) { + return; + } + await client.replyMessage(replyToken, flexMessage); + } else { + return; + } + }; お気に入り店の解除 機能 「行きつけを解除」をタップするとデータが消去されます。 ①「行きつけを解除」をタップする こちらはクライアント側の操作なので特にすることはありません。 ②user_idとtimestampを元にDynamoDBからデータを削除する こちらも同様にポストバックを使用します。 { "type": "postback", "label": "行きつけを解除", "data": `timestamp=${item.timestamp}`, "displayText": "行きつけを解除する", } こちらもsplitを使って値を取得しましょう。 次にDynamoDBの削除は、deleteを使用します。 api/src/Common/Database/DeleteFavorite.ts // Load the package import aws from 'aws-sdk'; // Create DynamoDB document client const docClient = new aws.DynamoDB.DocumentClient(); export const deleteFavorite = (data: string, userId: string | undefined) => { return new Promise((resolve, reject) => { // data const timestamp: number = Number(data.split('=')[1]); const params = { TableName: 'Gourmets_Favorite', Key: { user_id: userId, timestamp: timestamp, }, }; docClient.delete(params, (err, data) => { if (err) { reject(err); } else { resolve(data); } }); }); }; ではこの関数を読み込みましょう。 api/src/index.ts // Load the package import { ClientConfig, Client, WebhookEvent } from '@line/bot-sdk'; import aws from 'aws-sdk'; // Load the module // TemplateMessage import { yourLocationTemplate } from './Common/TemplateMessage/YourLocation'; import { errorTemplate } from './Common/TemplateMessage/Error'; import { isCarTemplate } from './Common/TemplateMessage/IsCar'; import { createFlexMessage } from './Common/TemplateMessage/Gourmet/CreateFlexMessage'; import { makeFlexMessage } from './Common/TemplateMessage/Favorite/MakeFlexMessage'; // Database import { putLocation } from './Common/Database/PutLocation'; import { updateIsCar } from './Common/Database/UpdateIsCar'; import { putFavorite } from './Common/Database/PutFavorite'; + import { deleteFavorite } from './Common/Database/DeleteFavorite'; // SSM const ssm = new aws.SSM(); const LINE_GOURMET_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_GOURMET_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const LINE_GOURMET_CHANNEL_SECRET = { Name: 'LINE_GOURMET_CHANNEL_SECRET', WithDecryption: false, }; const LINE_GOURMET_GOOGLE_MAP_API = { Name: 'LINE_GOURMET_GOOGLE_MAP_API', WithDecryption: false, }; exports.handler = async (event: any, context: any) => { // Retrieving values in the SSM parameter store const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_GOURMET_CHANNEL_ACCESS_TOKEN) .promise(); const CHANNEL_SECRET: any = await ssm.getParameter(LINE_GOURMET_CHANNEL_SECRET).promise(); const GOOGLE_MAP_API: any = await ssm.getParameter(LINE_GOURMET_GOOGLE_MAP_API).promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; const channelSecret: string = CHANNEL_SECRET.Parameter.Value; const googleMapApi: string = GOOGLE_MAP_API.Parameter.Value; // Create a client using the SSM parameter store const clientConfig: ClientConfig = { channelAccessToken: channelAccessToken, channelSecret: channelSecret, }; const client = new Client(clientConfig); // body const body: any = JSON.parse(event.body); const response: WebhookEvent = body.events[0]; // action try { await actionLocationOrError(client, response); await actionIsCar(client, response); await actionFlexMessage(client, response, googleMapApi); await actionPutFavoriteShop(response, googleMapApi); await actionTapFavoriteShop(client, response); + await actionDeleteFavoriteShop(response); } catch (err) { console.log(err); } }; // 位置情報もしくはエラーメッセージを送る const actionLocationOrError = async (client: Client, event: WebhookEvent): Promise<void> => { try { // If the message is different from the target, returned if (event.type !== 'message' || event.message.type !== 'text') { return; } // Retrieve the required items from the event const replyToken = event.replyToken; const text = event.message.text; // modules const yourLocation = await yourLocationTemplate(); const error = await errorTemplate(); // Perform a conditional branch if (text === 'お店を探す') { await client.replyMessage(replyToken, yourLocation); } else if (text === '車' || text === '徒歩') { return; } else if (text === '行きつけのお店') { return; } else { await client.replyMessage(replyToken, error); } } catch (err) { console.log(err); } }; // 移動手段の「車もしくは徒歩」かを尋ねるメッセージを送る const actionIsCar = async (client: Client, event: WebhookEvent): Promise<void> => { try { // If the message is different from the target, returned if (event.type !== 'message' || event.message.type !== 'location') { return; } // Retrieve the required items from the event const replyToken = event.replyToken; const userId = event.source.userId; const latitude: string = String(event.message.latitude); const longitude: string = String(event.message.longitude); // Register userId, latitude, and longitude in DynamoDB await putLocation(userId, latitude, longitude); // modules const isCar = await isCarTemplate(); // Send a two-choice question await client.replyMessage(replyToken, isCar); } catch (err) { console.log(err); } }; // 上記の選択を経て、おすすめのお店をFlex Messageにして送る const actionFlexMessage = async (client: Client, event: WebhookEvent, googleMapApi: string) => { try { // If the message is different from the target, returned if (event.type !== 'message' || event.message.type !== 'text') { return; } // Retrieve the required items from the event const replyToken = event.replyToken; const userId = event.source.userId; const isCar = event.message.text; // Perform a conditional branch if (isCar === '車' || isCar === '徒歩') { // Register userId, isCar in DynamoDB await updateIsCar(userId, isCar); const flexMessage = await createFlexMessage(userId, googleMapApi); if (flexMessage === undefined) { return; } await client.replyMessage(replyToken, flexMessage); } else { return; } } catch (err) { console.log(err); } }; // FlexMessageの「行きつけ」をタップしたらそのお店が登録される const actionPutFavoriteShop = async (event: WebhookEvent, googleMapApi: string) => { try { // If the message is different from the target, returned if (event.type !== 'postback') { return; } // Retrieve the required items from the event const data = event.postback.data; const timestamp = event.timestamp; const userId = event.source.userId; // conditional branching const isFavorite = data.indexOf('timestamp'); if (isFavorite === -1) { // Register data, userId in DynamoDB await putFavorite(data, timestamp, userId, googleMapApi); } } catch (err) { console.log(err); } }; // リッチメニューの「行きつけ」をタップしたらメッセージが送られる const actionTapFavoriteShop = async (client: Client, event: WebhookEvent) => { // If the message is different from the target, returned if (event.type !== 'message' || event.message.type !== 'text') { return; } // Retrieve the required items from the event const replyToken = event.replyToken; const userId = event.source.userId; const text = event.message.text; if (text === '行きつけのお店') { const flexMessage = await makeFlexMessage(userId); if (flexMessage === undefined) { return; } await client.replyMessage(replyToken, flexMessage); } else { return; } }; + // FlexMessageの「行きつけを解除」をタップしたらそのお店がDBから削除される + const actionDeleteFavoriteShop = async (event: WebhookEvent) => { + try { + // If the message is different from the target, returned + if (event.type !== 'postback') { + return; + } + + // Retrieve the required items from the event + const data = event.postback.data; + const userId = event.source.userId; + + // conditional branching + const isFavorite = data.indexOf('timestamp'); + if (isFavorite !== -1) { + // Delete Gourmets_Favorite + await deleteFavorite(data, userId); + } + } catch (err) { + console.log(err); + } + }; これで完了です。 すべての機能を盛り込みました。 これでアプリとしては十分使えると思います。 まぁまだ問題点はあります。 FlexMessageは1度で12個しかスクロールできません。 なので、お気に入り店舗が12以上になると表示する方法がありません。 12以上の場合は複数回返信を行うように設定してもいいのですが、 店舗数が増えれば増えるほど見辛くなる問題も孕んでいます。 ただでさえ1つで画面占有の6割以上です。 これを2つ、3つと増やした場合はユーザビリティの悪化に繋がります。 なので残念ながらこれ以上の対応は思いつかないということで、簡易的なお気に入り機能として使っていただけると幸いです。 終わりに LINE Messaging APIを使うことでフロントの開発から解放されます。 LINEという身近なツールを使うことでインストールなどの手間もなく、これがあると便利だなってものを簡単に制作することができます。 ぜひ皆さんもLINE Bot開発をしてみてください。 ここまで読んでいただきありがとうございました。

Viewing all articles
Browse latest Browse all 9055

Latest Images

Trending Articles