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

【個人開発/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つの記事に分かれています。 お店の検索を行うところまでを今回の記事で行っています。 お気に入り店の登録や解除などを次の記事で行います。 どのようなアプリか 皆さんは、どのようにして飲食店を探しますか? 私は、食べログなどのグルメサイトを使わずに Google Mapで探します。 以前食べログで「星 3.8 問題」がありました。 これだけではなく、食べログで見つけた行ったお店がイマイチだったこともあり、 グルメサイトはお店を探す場所ではなく、お店を予約するためのサイトと私は割り切りました。 電話が苦手な自分としては、まだまだ飲食店で独自の予約サイトを持っている企業も少ないので、食べログやホットペッパーで予約が可能なのはすごく助かっています。 Google Mapでお店を探すのもなかなか手間がかかるので、今回はGoogle Mapを使って近くの名店を10個教えてくれるアプリを作成しました。 Github 完成形のコードは以下となります。 アプリQR こちらから触ってみてください。 アプリの流れ クライアント LINE Messaging API(バックエンド) ①「お店を探す」をタップ ②「現在地を送る」ためのボタンメッセージを送信、例外時にはエラーメッセージを送信 ③ 現在地を送る ④「車か徒歩どちらですか?」というメッセージを送る ⑤ 車か徒歩を選択 ⑥ お店の配列を作成する(車の場合現在地から 14km 以内、徒歩の場合 0.8km 以内) ⑦ 必要なデータのみにする ⑧ 評価順に並び替えて上位 10 店舗にする ⑨ Flex Message を作成する ⑩ お店の情報を Flex Message で送る ハンズオン! 前提 初めてAWSを使う方に対しての注意です。 ルートユーザーで行うのはよろしくないので、全ての権限を与えたAdministratorユーザーを作っておいてください。 公式サイトはこちらです。 文章は辛いよって方は、初学者のハンズオン動画があるのでこちらからどうぞ。 sam initを実行する ゼロから書いていってもいいのですが、初めての方はまずはsam initを使いましょう。 以下のように選択していってください。 ターミナル $ sam init Which template source would you like to use? 1 - AWS Quick Start Templates 2 - Custom Template Location Choice: 1 What package type would you like to use? 1 - Zip (artifact is a zip uploaded to S3) 2 - Image (artifact is an image uploaded to an ECR image repository) Package type: 1 Which runtime would you like to use? 1 - nodejs14.x 2 - python3.8 3 - ruby2.7 4 - go1.x 5 - java11 6 - dotnetcore3.1 7 - nodejs12.x 8 - nodejs10.x 9 - python3.7 10 - python3.6 11 - python2.7 12 - ruby2.5 13 - java8.al2 14 - java8 15 - dotnetcore2.1 Runtime: 7 Project name [sam-app]: Gourmet AWS quick start application templates: 1 - Hello World Example 2 - Step Functions Sample App (Stock Trader) 3 - Quick Start: From Scratch 4 - Quick Start: Scheduled Events 5 - Quick Start: S3 6 - Quick Start: SNS 7 - Quick Start: SQS 8 - Quick Start: App Backend using TypeScript 9 - Quick Start: Web Backend Template selection: 1 ここまでできれば作成されます。 このような構成になっていればOKです。 .Gourmet ├── events/ │ ├── event.json ├── hello-world/ │ ├── tests │ │ └── integration │ │ │ └── test-api-gateway.js │ │ └── unit │ │ │ └── test-handler.js │ ├── .npmignore │ ├── app.js │ ├── package.json ├── .gitignore ├── README.md ├── template.yaml 必要ないファイルなどがあるのでそれを削除していきましょう。 .Gourmet ├── hello-world/ │ ├── app.js ├── .gitignore ├── README.md ├── template.yaml また、ディレクトリ名やファイル名を変えましょう。 .Gourmet ├── api/ │ ├── index.js ├── .gitignore ├── README.md ├── template.yaml 次は、template.yamlを修正して、SAMの実行をしてみたいところですが、一旦後回しにします。 先にTypeScriptなどのパッケージを入れ、ディレクトリ構造を明確にした後の方が理解しやすいので。。 ということでパッケージを入れていきましょう。 package.jsonの作成 以下のコマンドを入力してください。 これで、package.jsonの作成が完了します。 ターミナル $ npm init -y 必要なパッケージのインストール dependencies dependenciesはすべてのステージで使用するパッケージです。 今回使用するパッケージは以下の4つです。 ・@line/bot-sdk ・aws-sdk ・axios 以下のコマンドを入力してください。 これで全てのパッケージがインストールされます。 ターミナル $ npm install @line/bot-sdk aws-sdk axios --save devDependencies devDependenciesはコーディングステージのみで使用するパッケージです。 今回使用するパッケージは以下の5つです。 ・typescript ・@types/node ・ts-node ・rimraf ・npm-run-all 以下のコマンドを入力してください。 これで全てのパッケージがインストールされます。 ターミナル $ npm install -D typescript @types/node ts-node rimraf npm-run-all package.jsonにコマンドの設定を行う npm run buildでコンパイルを行います。 package.json { "scripts": { "clean": "rimraf dist", "tsc": "tsc", "build": "npm-run-all clean tsc" }, } tsconfig.jsonの作成 以下のコマンドを実行しTypeScriptの初期設定を行います。 ターミナル $ npx tsc --init それでは、作成されたtsconfig.jsonの上書きをしていきます。 tsconfig.json { "compilerOptions": { "target": "ES2018", "module": "commonjs", "sourceMap": true, "outDir": "./api/dist", "strict": true, "moduleResolution": "node", "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["api/src/*"] } 簡単にまとめると、 api/srcディレクトリ以下を対象として、それらをapi/distディレクトリにES2018の書き方でビルドされるという設定です。 tsconfig.jsonに関して詳しく知りたい方は以下のサイトをどうぞ。 また、この辺りで必要ないディレクトリはGithubにpushしたくないので、.gitignoreも作成しておきましょう。 .gitignore node_modules package-lock.json .aws-sam samconfig.toml dist 最終的にはこのようなディレクトリ構成にしましょう。 .Gourmet ├── api/ │ ├── dist(コンパイル後) │ │ └── node_modules(コピーする) │ │ └── package.json(コピーする) │ ├── src(コンパイル前) │ │ └── index.ts ├── node_modules(コピー元) ├── .gitignore ├── package.json(コピー元) ├── package-lock.json ├── README.md ├── template.yaml ├── tsconfig.json やるべきことは以下の2つです。 ①distディレクトリを作成する ②distディレクトリに、node_modules, package.jsonをコピーする 次に、template.yamlを書いていきましょう。 SAM Templateを記載する ファイル内にコメントを残しています。 これで大まかには理解できるかと思います。 詳しくは公式サイトを見てください。 template.yaml # AWS CloudFormationテンプレートのバージョン AWSTemplateFormatVersion: '2010-09-09' # CloudFormationではなくSAMを使うと明記する Transform: AWS::Serverless-2016-10-31 # CloudFormationのスタックの説明文(重要ではないので適当でOK) Description: > LINE Messaging API + Node.js + TypeScript + SAM(Lambda, API Gateway, DynamoDB, S3)で作った飲食店検索アプリです Globals: # Lambda関数のタイムアウト値(3秒に設定) Function: Timeout: 3 Resources: # API Gateway GourmetAPI: # Typeを指定する(今回はAPI Gateway) Type: AWS::Serverless::Api Properties: # ステージ名(APIのURLの最後にこのステージ名が付与されます) StageName: v1 # Lambda GourmetFunction: # Typeを指定する(今回はLambda) Type: AWS::Serverless::Function Properties: # 関数が格納されているディレクトリ(今回はコンパイル後なので、distディレクトリを使用する) CodeUri: api/dist # ファイル名と関数名(今回はファイル名がindex.js、関数名がexports.handlerなので、index.handlerとなります) Handler: index.handler # どの言語とどのバージョンを使用するか Runtime: nodejs12.x # ポリシーを付与する(今回はLambdaの権限とSSMの読み取り権限を付与) Policies: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess # この関数をトリガーするイベントを指定します Events: # API Gateway GourmetAPI: Type: Api Properties: # どのAPIを使用するか(!Refは値の参照に使用します) RestApiId: !Ref GourmetAPI # URL Path: / # POSTメソッド Method: post Outputs: GourmetAPI: Description: 'API Gateway' Value: !Sub 'https://${GourmetAPI}.execute-api.${AWS::Region}.amazonaws.com/v1' GourmetFunction: Description: 'Lambda' Value: !GetAtt GourmetFunction.Arn GourmetFunctionIamRole: Description: 'IAM Role' Value: !GetAtt GourmetFunctionRole.Arn LINE Developersにアカウントを作成する LINE Developersにアクセスして、「ログイン」ボタンをクリックしてください。 その後諸々入力してもらったら以下のように作成できるかと思います。 注意事項としては、今回Messaging APIとなるので、チャネルの種類を間違えた方は修正してください。 チャネルシークレットとチャネルアクセストークンが必要になるのでこの2つを発行します。 これで必要な環境変数は取得できました。 それでは、これをSSMを使ってLambda内で使えるようにしていきましょう。 SSMパラメータストアで環境変数を設定 なぜSSMパラメータストアを使うのか? SAMのLambda設定にも、環境変数の項目はあります。 しかし、2点問題点があります。 ①Lambdaの環境変数の変更をしたいとき、Lambdaのバージョンも新規発行をしなければならない ②Lambdaのバージョンとエイリアスを紐付けて管理をするとき、もし環境変数にリリース先環境別の値をセットしていると、リリース時に手動で環境変数の変更をしなければならないケースが発生する 簡単にまとめると、「リアルタイムで反映できないし、人為的なミスのリスクもあるよ」ということです。 SSMパラメータストアで値を管理すると以下の3点のメリットがあります。 ①Lambdaの環境変数の管理が不要 ②Lambdaも含めた値関連情報を一元管理できる ③Lambda外部からリアルタイムに環境変数を変更制御できる ということで、SSMパラメータストアを使用しましょう。 みんな大好きクラスメソッドの記事にやり方が書いてあります。 こちらの記事が完璧なのでこちらを見てやってみてください。 私は以下のように命名して作成しました。 SSMパラメータが取得できているかconsole.logで検証 api/src/index.ts // import import aws from 'aws-sdk'; // SSM const ssm = new aws.SSM(); exports.handler = async (event: any, context: any) => { const LINE_GOURMET_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_GOURMET_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_GOURMET_CHANNEL_ACCESS_TOKEN) .promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; console.log('channelAccessToken: ' + channelAccessToken); }; これをコンパイルしてデプロイしていきましょう。 ターミナル // コンパイル $ npm run build // ビルド $ sam build // デプロイ $ sam deploy --guided Configuring SAM deploy ====================== Looking for samconfig.toml : Not found Setting default arguments for 'sam deploy' ========================================= // CloudFormation スタック名の指定 Stack Name [sam-app]: Gourmet // リージョンの指定 AWS Region [us-east-1]: ap-northeast-1 // デプロイ前にCloudformationの変更セットを確認するか #Shows you resources changes to be deployed and require a 'Y' to initiate deploy Confirm changes before deploy [y/N]: y // SAM CLI に IAM ロールの作成を許可するか(CAPABILITY_IAM) #SAM needs permission to be able to create roles to connect to the resources in your template Allow SAM CLI IAM role creation [Y/n]: y // API イベントタイプの関数に認証が含まれていない場合、警告される HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: y // この設定を samconfig.toml として保存するか Save arguments to samconfig.toml [Y/n]: y これでデプロイが完了します。 では、API GatewayのURLを確認しましょう。 Webhook URLの登録 先ほどAPI Gatewayで作成したhttpsのURLをコピーしてください。 これをLINE DevelopersのWebhookに設定します。 それではSSMパラメータが正しく取得できているか確認しましょう。 CloudWatchで確認しましょう! 取得できていますね! これで準備は完了です。 ここから飲食店検索の仕組みを作っていきましょう! アプリの流れ クライアント LINE Messaging API(バックエンド) ①「お店を探す」をタップ ②「現在地を送る」ためのボタンメッセージを送信、例外時にはエラーメッセージを送信 ③ 現在地を送る ④「車か徒歩どちらですか?」というメッセージを送る ⑤ 車か徒歩を選択 ⑥ お店の配列を作成する(車の場合現在地から 14km 以内、徒歩の場合 0.8km 以内) ⑦ 必要なデータのみにする ⑧ 評価順に並び替えて上位 10 店舗にする ⑨ Flex Message を作成する ⑩ お店の情報を Flex Message で送る ①「お店を探す」をタップ こちらに関してはクライアント側の操作なので作業することはありません。 ②「現在地を送る」ためのボタンメッセージを送信、例外時にはエラーメッセージを送信 「現在地を送る」ためのボタンメッセージ api/src/Common/TemplateMessage/YourLocation.ts // Load the package import { TemplateMessage } from '@line/bot-sdk'; export const yourLocationTemplate = (): Promise<TemplateMessage> => { return new Promise((resolve, reject) => { const params: TemplateMessage = { type: 'template', altText: '現在地を送ってください!', template: { type: 'buttons', text: '今日はどこでご飯を食べる?', actions: [ { type: 'uri', label: '現在地を送る', uri: 'https://line.me/R/nv/location/', }, ], }, }; resolve(params); }); }; ちなみに以下のURLですが、LINEで利用できるURLスキームというもので位置情報を送れるものです。 https://line.me/R/nv/location/ 詳しくは以下をご確認ください。 エラーメッセージ api/src/Common/TemplateMessage/Error.ts // Load the package import { TextMessage } from '@line/bot-sdk'; export const errorTemplate = (): Promise<TextMessage> => { return new Promise((resolve, reject) => { const params: TextMessage = { type: 'text', text: 'ごめんなさい、このメッセージには対応していません', }; resolve(params); }); }; メッセージの送信 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'; // 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, }; 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 channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; const channelSecret: string = CHANNEL_SECRET.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); } 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 { await client.replyMessage(replyToken, error); } } catch (err) { console.log(err); } }; ③ 現在地を送る こちらに関してもクライアント側の操作なので作業することはありません。 ④「車か徒歩どちらですか?」というメッセージを送る LINE Messaging APIにキャッシュの機能などはありません。 なので、③の「現在地を送る」のデータはどこかに格納しないと値が消えてしまいます。 ということで、今回はサーバーレスと相性の良い「DynamoDB」を使用します。 DynamoDB 以下のテーブルを作成します。 PK K K K user_id latitude longitude is_car ユーザー ID 緯度 経度 車か徒歩か それぞれのデータ取得方法 ユーザーIDは、event.source.userIdから取得できます。 緯度、経度は、【クライアント】③ 現在地を送るから取得できます。 車か徒歩かは、【クライアント】⑤ 車か徒歩を選択から取得できます。 SAMテンプレートにDynamoDBの記載を行う template.yaml # AWS CloudFormationテンプレートのバージョン AWSTemplateFormatVersion: '2010-09-09' # CloudFormationではなくSAMを使うと明記する Transform: AWS::Serverless-2016-10-31 # CloudFormationのスタックの説明文(重要ではないので適当でOK) Description: > LINE Messaging API + Node.js + TypeScript + SAM(Lambda, API Gateway, DynamoDB, S3)で作った飲食店検索アプリです Globals: # Lambda関数のタイムアウト値(3秒に設定) Function: Timeout: 3 Resources: # API Gateway GourmetAPI: # Typeを指定する(今回はAPI Gateway) Type: AWS::Serverless::Api Properties: # ステージ名(APIのURLの最後にこのステージ名が付与されます) StageName: v1 + # DynamoDB + GourmetDynamoDB: + # Typeを指定する(今回はDynamoDB) + Type: AWS::Serverless::SimpleTable + Properties: + # テーブルの名前 + TableName: Gourmets + # プライマリキーの設定(名前とプライマリキーのタイプ) + PrimaryKey: + Name: user_id + Type: String + # プロビジョニングされたキャパシティの設定(今回の要件では最小の1でOK) + ProvisionedThroughput: + ReadCapacityUnits: 1 + WriteCapacityUnits: 1 # Lambda GourmetFunction: # Typeを指定する(今回はLambda) Type: AWS::Serverless::Function Properties: # 関数が格納されているディレクトリ(今回はコンパイル後なので、distディレクトリを使用する) CodeUri: api/dist # ファイル名と関数名(今回はファイル名がindex.js、関数名がexports.handlerなので、index.handlerとなります) Handler: index.handler # どの言語とどのバージョンを使用するか Runtime: nodejs12.x # ポリシーを付与する(今回はLambdaの権限とSSMの読み取り権限とDynamoDBのフルアクセス権限を付与) Policies: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess + - arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess # この関数をトリガーするイベントを指定します Events: # API Gateway GourmetAPI: Type: Api Properties: # どのAPIを使用するか(!Refは値の参照に使用します) RestApiId: !Ref GourmetAPI # URL Path: / # POSTメソッド Method: post Outputs: GourmetAPI: Description: 'API Gateway' Value: !Sub 'https://${GourmetAPI}.execute-api.${AWS::Region}.amazonaws.com/v1' GourmetFunction: Description: 'Lambda' Value: !GetAtt GourmetFunction.Arn GourmetFunctionIamRole: Description: 'IAM Role' Value: !GetAtt GourmetFunctionRole.Arn 現在地が送信されたらDynamoDBのuser_id, latitude, longitudeが入力されるようにする 今回はDynamoDBに新規のレコードを追加します。 新規追加はputを使用します。 api/src/Common/Database/PutLocation.ts // Load the package import aws from 'aws-sdk'; // Create DynamoDB document client const docClient = new aws.DynamoDB.DocumentClient(); export const putLocation = (userId: string | undefined, latitude: string, longitude: string) => { return new Promise((resolve, reject) => { const params = { Item: { user_id: userId, latitude: latitude, longitude: longitude, }, ReturnConsumedCapacity: 'TOTAL', TableName: 'Gourmets', }; docClient.put(params, (err, data) => { if (err) { reject(err); } else { resolve(data); } }); }); }; この関数を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'; // Database + import { putLocation } from './Common/Database/PutLocation'; // 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, }; 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 channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; const channelSecret: string = CHANNEL_SECRET.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); } 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 { 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); + } catch (err) { + console.log(err); + } + }; これでDynamoDBへの登録が完了です。 次にメッセージを作成しましょう。 api/src/Common/TemplateMessage/IsCar.ts // Load the package import { TemplateMessage } from '@line/bot-sdk'; export const isCarTemplate = (): Promise<TemplateMessage> => { return new Promise((resolve, reject) => { const params: TemplateMessage = { type: 'template', altText: 'あなたの移動手段は?', template: { type: 'confirm', text: 'あなたの移動手段は?', actions: [ { type: 'message', label: '車', text: '車', }, { type: 'message', label: '徒歩', text: '徒歩', }, ], }, }; resolve(params); }); }; 最後にこちらの関数を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'; // Database import { putLocation } from './Common/Database/PutLocation'; // 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, }; 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 channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; const channelSecret: string = CHANNEL_SECRET.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); } 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 { 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); } }; ⑤ 車か徒歩を選択 こちらに関してもクライアント側の操作なので作業することはありません。 ⑥ お店の配列を作成する 車の場合現在地から 14km以内、徒歩の場合 0.8km以内で検索することとします。 車は20分程度、徒歩は10分程度で着く範囲を検索対象としています。 移動手段が送信されたらDynamoDBのis_carが入力されるようにする 今回はDynamoDBにuser_idをキーとして、レコードを更新します。 更新はupdateを使用します。 ではやっていきましょう。 api/src/Common/Database/UpdateIsCar.ts // Load the package import aws from 'aws-sdk'; // Create DynamoDB document client const docClient = new aws.DynamoDB.DocumentClient(); export const updateIsCar = (userId: string | undefined, isCar: string) => { return new Promise((resolve, reject) => { const params = { TableName: 'Gourmets', Key: { user_id: userId, }, UpdateExpression: 'SET is_car = :i', ExpressionAttributeValues: { ':i': isCar, }, }; docClient.update(params, (err, data) => { if (err) { reject(err); } else { resolve(data); } }); }); }; このDB処理を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'; // Database import { putLocation } from './Common/Database/PutLocation'; + import { updateIsCar } from './Common/Database/UpdateIsCar'; // 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, }; 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 channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; const channelSecret: string = CHANNEL_SECRET.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); } 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 { 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); } }; + const actionFlexMessage = async (client: Client, event: WebhookEvent) => { + 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); + } else { + return; + } + } catch (err) { + console.log(err); + } + }; お店の配列を作成するまでのステップ 1. DynamoDBのデータを取得する api/src/Common/TemplateMessage/Gourmet/GetDatabaseInfo.ts // Load the package import aws from 'aws-sdk'; // Create DynamoDB document client const docClient = new aws.DynamoDB.DocumentClient(); export const getDatabaseInfo = async (userId: string | undefined) => { return new Promise((resolve, reject) => { const params = { TableName: 'Gourmets', Key: { user_id: userId, }, }; docClient.get(params, (err, data) => { if (err) { reject(err); } else { resolve(data); } }); }); }; 2. Google Map APIを取得して、SSMパラメーターストアに登録する Google MapのAPIを取得しましょう。 まずはGCPのコンソール画面に入って下さい。 コンソールに入ったらプロジェクトを作成しましょう! 私は、LINE-Node-TypeScript-Gourmetで作成しました。 では、ライブラリを有効化しましょう! 使うライブラリは2つです。 Map JavaScript API Places API お店検索をするAPIは「Places API」ですが、 JavaScriptから呼び出すために「Map JavaScript API」が必要となります。 ここまでできたら次にAPIを作成しましょう。 これからの開発はこちらのAPIキーを使います。 セキュリティ的には制限をつけたほうがいいのですが、今回はつけずに行います。 上記の説明でわからなければ以下のサイトを参考にされて下さい。 では取得したAPIをSSMパラメーターストアに登録しましょう。 方法は以下の通りです。 私はこのように命名しました。 ではこの値を関数内で使えるようにしましょう。 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'; // Database import { putLocation } from './Common/Database/PutLocation'; import { updateIsCar } from './Common/Database/UpdateIsCar'; // 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); } 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 { 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); } }; + 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); } else { return; } } catch (err) { console.log(err); } }; 3. お店の配列を作成する 近隣のお店を調べるので、Place SearchのNearby Search requestsを使います。 ここが正直イマイチなコードかもしれません。 setTimeoutを頻発しているからです。 Nearby Search requestsは20店舗しか取り出すことができないのですが、 pagetokenを使用することで60店舗取り出すことができます。 このpagetokenを使って再度呼び出しを行うのですが、その時に待ち時間が必要になります。 最初は、async, awaitの非同期で対応できると思っていたのですが、この待ち時間だけでは足りないようでsetTimeoutが必要になりました。 こちらはコードがイマイチなので、対応を考えて他の方法があれば修正いたします。 ここはこんなコードの書き方もあるんだ程度にしていただけますと幸いです。 api/src/Common/TemplateMessage/Gourmet/GetGourmetInfo.ts // Load the package import axios, { AxiosResponse } from 'axios'; // Load the module import { getDatabaseInfo } from './GetDatabaseInfo'; export const getGourmetInfo = async (user_id: string | undefined, googleMapApi: string) => { return new Promise(async (resolve, reject) => { // modules getDatabaseInfo const data: any = await getDatabaseInfo(user_id); const isCar = data.Item.is_car; const latitude = data.Item.latitude; const longitude = data.Item.longitude; // Bifurcate the radius value depending on whether you are driving or walking let radius = 0; if (isCar === '車') { radius = 1400; } else { radius = 800; } let gourmetArray: any[] = []; const url = `https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=${latitude},${longitude}&radius=${radius}&type=restaurant&key=${googleMapApi}&language=ja`; new Promise(async (resolve) => { const gourmets: AxiosResponse<any> = await axios.get(url); const gourmetData = gourmets.data.results; gourmetArray = gourmetArray.concat(gourmetData); const pageToken = gourmets.data.next_page_token; resolve(pageToken); }) .then((value) => { return new Promise((resolve) => { setTimeout(async () => { const addTokenUrl = `${url}&pagetoken=${value}`; const gourmets = await axios.get(addTokenUrl); const gourmetData = gourmets.data.results; gourmetArray = gourmetArray.concat(gourmetData); const pageToken = gourmets.data.next_page_token; resolve(pageToken); }, 2000); }); }) .then((value) => { setTimeout(async () => { const addTokenUrl = `${url}&pagetoken=${value}`; const gourmets = await axios.get(addTokenUrl); const gourmetData = gourmets.data.results; gourmetArray = gourmetArray.concat(gourmetData); }, 2000); }); setTimeout(() => { resolve(gourmetArray); }, 8000); }); }; ⑦ 必要なデータのみにする 使うデータは以下の通りです。 必要なデータ 理由 geometry_location_lat 店舗案内のURLで使うため geometry_location_lng 店舗案内のURLで使うため name Flex Message内と店舗詳細のURLで使うため photo_reference 店舗写真を生成するために使う rating Flex Message内で使うため vicinity 店舗詳細のURLで使うため 店舗詳細と店舗案内、店舗写真のURLはこの後解説します。 ということで必要なデータのみを抜き出して配列を再生成しましょう。 api/src/Common/TemplateMessage/Gourmet/FormatGourmetArray.ts // Load the module import { getGourmetInfo } from './GetGourmetInfo'; // types import { RequiredGourmetArray } from './types/FormatGourmetArray.type'; export const formatGourmetArray = async ( user_id: string | undefined, googleMapApi: string ): Promise<RequiredGourmetArray> => { return new Promise(async (resolve, reject) => { // modules getGourmetInfo const gourmetInfo: any = await getGourmetInfo(user_id, googleMapApi); // Extract only the data you need const sufficientGourmetArray: any = gourmetInfo.filter( (gourmet: any) => gourmet.photos !== undefined || null ); // Format the data as required const requiredGourmetArray: RequiredGourmetArray = sufficientGourmetArray.map( (gourmet: any) => { return { geometry_location_lat: gourmet.geometry.location.lat, geometry_location_lng: gourmet.geometry.location.lng, name: gourmet.name, photo_reference: gourmet.photos[0].photo_reference, rating: gourmet.rating, vicinity: gourmet.vicinity, }; } ); resolve(requiredGourmetArray); }); }; 上記で、RequiredGourmetArrayという型を使用しているので型定義ファイルを作ります。 api/src/Common/TemplateMessage/Gourmet/types/FormatGourmetArray.type.ts export type RequiredGourmetArray = { geometry_location_lat: number; geometry_location_lng: number; name: string; photo_reference: string; rating: number; vicinity: string; }[]; ⑧ 評価順に並び替えて上位10店舗にする sortで並び替えて、sliceで新たな配列を作ってあげましょう! api/src/Common/TemplateMessage/Gourmet/SortRatingGourmetArray.ts // Load the module import { formatGourmetArray } from './FormatGourmetArray'; // types import { GourmetData, GourmetDataArray } from './types/SortRatingGourmetArray.type'; export const sortRatingGourmetArray = async ( user_id: string | undefined, googleMapApi: string ): Promise<GourmetDataArray> => { return new Promise(async (resolve, reject) => { try { // modules formatGourmetArray const gourmetArray: GourmetDataArray = await formatGourmetArray(user_id, googleMapApi); // Sort by rating gourmetArray.sort((a: GourmetData, b: GourmetData) => b.rating - a.rating); // narrow it down to 10 stores. const sortGourmetArray: GourmetDataArray = gourmetArray.slice(0, 10); console.log(sortGourmetArray); resolve(sortGourmetArray); } catch (err) { reject(err); } }); }; 型定義を行いましょう。 api/src/Common/TemplateMessage/Gourmet/types/SortRatingGourmetArray.type.ts export type GourmetData = { geometry_location_lat: number; geometry_location_lng: number; name: string; photo_reference: string; rating: number; vicinity: string; }; export type GourmetDataArray = GourmetData[]; ⑨ Flex Messageを作成する ⑦で説明した必要なデータについて解説します。 必要なデータ 理由 geometry_location_lat 店舗案内のURLで使うため geometry_location_lng 店舗案内のURLで使うため name Flex Message内と店舗詳細のURLで使うため photo_reference 店舗写真を生成するために使う rating Flex Message内で使うため vicinity 店舗詳細のURLで使うため nameとratingはFlex Message内で使います。 店舗詳細に関してですが、こちらのURLは以下となります。 https://maps.google.co.jp/maps?q=${店舗名}${住所}&z=15&iwloc=A 店舗案内に関しては以下のURLとなります。 https://www.google.com/maps/dir/?api=1&destination=${緯度},${経度} 店舗写真に関しては以下のURLとなります。 https://maps.googleapis.com/maps/api/place/photo?maxwidth=${任意の幅}&photoreference=${photo_reference}&key=${Google_API} ということで、Flex Message内でこれらのURLを生成していけば完成です。 やっていきましょう! 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, }, }, ], 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); } }); }; 型定義を行いましょう。 api/src/Common/TemplateMessage/Gourmet/types/CreateFlexMessage.type.ts export type Gourmet = { geometry_location_lat: number; geometry_location_lng: number; name: string; photo_reference: string; rating: number; vicinity: string; }; export type RatingGourmetArray = Gourmet[]; ⑩ お店の情報をFlex Messageで送る 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'; // 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); } 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 { 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); } }; 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); } }; デプロイ まずは、npm run buildでコンパイルしましょう。 ターミナル $ npm run build コンパイルされた後は、ビルドしてデプロイしていきましょう。 ターミナル // ビルド $ sam build // デプロイ $ sam deploy --guided DynamoDBも確認しましょう。 しっかり保存されていますね! 最後に 追加する要件として、今後はお気に入りのお店を登録する機能なども足していこうと思います。 ここまで読んでいただきありがとうございました!

Viewing all articles
Browse latest Browse all 9086

Latest Images

Trending Articles