【Node.js, TypeScript】LINE MessagingAPIで作った天気予報アプリをAWSのLambdaにデプロイしてみる！

先日、Node.jsとTypeScriptで天気予報アプリを作成しました。

https://qiita.com/Ryo9597/items/4c05b8b972ad7efc0c66

完成形としては以下の通りです。
![名称未設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/0af0bfce-b695-b341-4e90-aeefde544562.png)

この記事内では、`Glitch`でデプロイをしています。
まぁ無料なわけで色々問題があります。
・プロジェクトは、利用されていないときは5分でスリープ状態になる
・4000件/1hのリクエスト制限がある(Error: 429 too many requests)

ということで、AWSのLambdaを使ってデプロイすることにします。
それではアーキテクチャに関してみていきましょう。

## サーバーレスアーキテクチャとは
AWSにおけるサーバーレスとは、**「インスタンスベースの仮想サーバー（EC2など）を使わずにアプリケーションを開発するアーキテクチャ」**を指します。

一般にシステムの運用には、プログラムを動かすためのサーバーが必要です。
そしてそのサーバーは、常に稼働していなければなりません。

しかし開発者がやりたいことは、「サーバーの管理」なのでしょうか？
エンドユーザーに価値を届けることこそが使命なわけです。

ということで、こういうめんどくさい作業から解放してくれるのがサーバーレスアーキテクチャなわけです。

サーバーレスアーキテクチャでよく使われるサービスは以下の通りです。
特に、丸で囲っている3つがよく使われます。

![スクリーンショット 2021-06-26 15.43.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/4d71d546-5839-0784-8cd0-d8d4466e915c.png)

今回は、`Lambda`, `API Gateway`, `S3`の3つを使ってアプリを作成していきます。

### アーキテクチャ

![arch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/e1cceafa-f7d4-a021-ef76-8f91a4c37e29.png)


## 追記
AWSのEC2を使ってデプロイした記事もあります。
サーバーレスよりもEC2に興味があるぞという方はこちらの記事もどうぞ。

https://qiita.com/Ryo9597/items/3fd188c4a8c7089c12f8

## どのようなアプリか
皆さんは、今日の気温を聞いて、**「快適に過ごすために今日のファッションをこうしよう」**ってパッと思いつきますか？

私は、最高気温、最低気温を聞いてもそれがどのくらい暑いのか、寒いのかがピンと来ず、洋服のチョイスを外したことがしばしばあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/c9583e2c-1e6c-a662-e7cf-b9995ca8e3cb.png)

こんな思いを2度としないために今回このアプリを作りました。

`line-bot-sdk-nodejs`の型定義で多少躓きましたが、TypeScript初心者でもそこまで時間かからずにできるかと思います。
なので、TypeScriptを勉強中の方はぜひ取り組んでみてください。

https://github.com/line/line-bot-sdk-nodejs/blob/master/lib/types.ts

## アプリの流れ
アプリの流れは大まかに以下の4つのステップで成り立っています。

・①クライアントが現在地を送る
・②[OpenWeather](https://openweathermap.org/)から天気予報を取得
・③データの整形
・④クライアントに送る

## GitHub
完成形のコードは以下となります。

https://github.com/ssk9597/LINE-Lambda-Node-Weather-Fashion

## では実際に作成していきましょう！
### AWSの環境構築を行う
上記で説明した通り、今回はAWSの3つのサービスを使用します。
その3つのサービスの環境を構築しましょう。

#### Lambda
まずは関数を作成しましょう。

![スクリーンショット 2021-07-14 15.41.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/592ddc84-3053-4a42-e7a6-1b1c4f69b32d.png)

では設定を行います。

![スクリーンショット 2021-07-14 15.45.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/07c4077c-471b-aa65-3d21-65f8e732d2f0.png)

これで終了です。

#### API Gateway
APIを作成しましょう。

![スクリーンショット 2021-07-14 15.48.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/cb8ae38b-de85-1f05-2e52-7d0036a5a5d2.png)

`REST API`を選択します。

![スクリーンショット 2021-07-14 15.51.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/c814ffa1-7d76-6a79-ee44-3b3235689403.png)

適当にAPI名をつけて作成しましょう。

![スクリーンショット 2021-07-14 15.52.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/38550ff5-5ddc-c0e1-70b9-886cf06bed59.png)

作成できたらメソッドを作成しましょう。

![スクリーンショット 2021-07-14 15.53.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/b99a85a4-5501-f728-3e5c-4f3e3afedb60.png)

メソッドは`POST`になります。

![スクリーンショット 2021-07-14 15.54.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/032f9fb6-06c9-97d6-e301-3ddad2f9a4ce.png)

Lambda関数を呼び出せるようにします。

![スクリーンショット 2021-07-14 15.55.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/b61df3ac-fcb7-cdc4-0989-aefe03a88da7.png)

最後にデプロイをしてURLを発行します。

![スクリーンショット 2021-07-14 15.58.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/1987aeca-548d-39b5-e14a-e99be5281fd6.png)

URLができました。
このURLを叩くと、Lambda関数が実行されることになります。

![スクリーンショット 2021-07-14 15.59.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/3ab6cd33-9860-8c8e-d95b-1b9fc2062768.png)

#### S3
バケットの作成を行います。

![スクリーンショット 2021-07-14 16.31.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/633ddd6b-37c9-ac78-72ed-fa18c1810984.png)

これで完了です。

### `LINE Developers`にアカウントを作成する
[LINE Developers](https://developers.line.biz/ja/)にアクセスして、「ログイン」ボタンをクリックしてください。

その後諸々入力してもらったら以下のように作成できるかと思います。
注意事項としては、今回`Messaging API`となるので、**チャネルの種類**を間違えた方は修正してください。

![スクリーンショット 2021-05-29 16.13.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/c255d10d-2dcf-15ed-1fea-fdbeed6da750.png)

**チャネルシークレット**と**チャネルアクセストークン**が必要になるのでこの2つを発行します。

![スクリーンショット 2021-05-29 16.16.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/ede74ac9-add0-c180-016d-efcab62e912a.png)

![スクリーンショット 2021-05-29 16.17.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/b5c7bf10-1726-28df-0eec-99a1447f7e11.png)

ではこの2つを`Lambdaの環境変数`に入力します。

![スクリーンショット 2021-07-14 16.08.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/e0565e58-3581-1a6f-9f17-8d893381c5a3.png)

### `package.json`の作成
以下のコマンドを入力してください。
これで、`package.json`の作成が完了します。

```terminal:ターミナル
$ npm init -y
```

### 必要なパッケージのインストール
#### dependencies
`dependencies`はすべてのステージで使用するパッケージです。

今回使用するパッケージは以下の4つです。
・@line/bot-sdk
・dotenv
・axios

以下のコマンドを入力してください。
これで全てのパッケージがインストールされます。

```terminal:ターミナル
$ npm install @line/bot-sdk dotenv axios --save
```

#### devDependencies
`devDependencies`はコーディングステージのみで使用するパッケージです。

今回使用するパッケージは以下の5つです。
・typescript
・@types/node
・ts-node
・rimraf
・npm-run-all

以下のコマンドを入力してください。
これで全てのパッケージがインストールされます。

```terminal:ターミナル
$ npm install -D typescript @types/node ts-node rimraf npm-run-all
```

### `package.json`にコマンドの設定を行う

`npm run dev`が開発環境の立ち上げに使います。
`npm run start`が本番環境用です。

```json:package.json
{
  "scripts": {
    "clean": "rimraf dist",
    "tsc": "tsc",
    "build": "npm-run-all clean tsc"
  },
}
```

### `tsconfig.json`の作成
以下のコマンドを実行しTypeScriptの初期設定を行います。

```terminal:ターミナル
$ npx tsc --init
```

それでは、作成された`tsconfig.json`の上書きをしていきます。

```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に関して詳しく知りたい方は以下のサイトをどうぞ。

https://book.yyts.org/handson/tsconfig.json-settings


また、この辺りで必要ないディレクトリはGithubにpushしたくないので、`.gitignore`も作成しておきましょう。

```.gitignore:.gitignore
_.drawio
node_modules
package-lock.json
dist
```

### Webhook URLの登録
先ほど`API Gateway`で作成したhttpsのURLをコピーしてください。

これをLINE DevelopersのWebhookに設定します。

![スクリーンショット 2021-07-14 16.30.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/b715a6aa-6dfe-d778-3f9e-3783103533b6.png)

これで初期設定は完了です。

ここからの流れはこのような感じです。

**①「今日の洋服は？」というメッセージを受け取る**
**②「今日の洋服は？」を受け取ったら、位置情報メッセージを送る**
**③「今日の洋服は？」以外を受け取ったら、「そのメッセージには対応していません」と送る**
**④「位置情報メッセージ」を受け取る**
**⑤「位置情報メッセージ」を受け取ったら、緯度と経度を使って天気予報を取得する**
**⑥「位置情報メッセージ」を受け取ったら、天気予報メッセージを送る**

では作っていきましょう！
またこれら全てのコードを`api/src/index.ts`に書くとコードが肥大化し可読性が落ちます。
なので`Common`ディレクトリに関数に切り分けて作成していきます。

またここからはLINEBotのオリジナルの型が頻出します。
1つずつ説明するのはあまりに時間がかかるので、知らない型が出てきたらその度に以下のサイトで検索するようにしてください。

https://github.com/line/line-bot-sdk-nodejs/blob/master/lib/types.ts

### ①「今日の洋服は？」というメッセージを受け取る

```typescript:api/src/index.ts
// パッケージのインストール
import { ClientConfig, Client, WebhookEvent } from '@line/bot-sdk';

// アクセストークンとチャンネルシークレットをenvから読み込む
const clientConfig: ClientConfig = {
  channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '',
  channelSecret: process.env.CHANNEL_SECRET || '',
};

// インスタンス化
const client: Client = new Client(clientConfig);

// 実行
exports.handler = async (event: any, context: any) => {
  const body: any = JSON.parse(event.body);
  const response: WebhookEvent = body.events[0];

  try {
    await actionButtonOrErrorMessage(response);
  } catch (err) {
    console.log(err);
  }
};

// ボタンメッセージもしくはエラーメッセージを送る関数
const actionButtonOrErrorMessage = async (event: WebhookEvent) => {
  try {
    if (event.type !== 'message' || event.message.type !== 'text') {
      return;
    }

    const { replyToken } = event;
    const { text } = event.message;

    if (text === '今日の洋服は？') {
      // 「今日の洋服は？」というメッセージを受け取る
    } else {
      // 「今日の洋服は？」以外のメッセージを受け取る
    }
  } catch (err) {
    console.log(err);
  }
};
```

### ②「今日の洋服は？」を受け取ったら、位置情報メッセージを送る

```typescript:api/src/Common/ButtonMessage/ButtonMessageTemplate.ts
// パッケージを読み込む
import { TemplateMessage } from '@line/bot-sdk';

export const buttonMessageTemplate = (): Promise<TemplateMessage> => {
  return new Promise((resolve, reject) => {
    const params: TemplateMessage = {
      type: 'template',
      altText: 'This is a buttons template',
      template: {
        type: 'buttons',
        text: '今日はどんな洋服にしようかな',
        actions: [
          {
            type: 'uri',
            label: '現在地を送る',
            uri: 'https://line.me/R/nv/location/',
          },
        ],
      },
    };
    resolve(params);
  });
};
```

```typescript:api/src/index.ts
// パッケージのインストール
import { ClientConfig, Client, WebhookEvent } from '@line/bot-sdk';

// モジュールを読み込む
import { buttonMessageTemplate } from './Common/ButtonMessage/ButtonMessageTemplate';

// アクセストークンとチャンネルシークレットをenvから読み込む
const clientConfig: ClientConfig = {
  channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '',
  channelSecret: process.env.CHANNEL_SECRET || '',
};

// インスタンス化
const client: Client = new Client(clientConfig);

// 実行
exports.handler = async (event: any, context: any) => {
  const body: any = JSON.parse(event.body);
  const response: WebhookEvent = body.events[0];

  try {
    await actionButtonOrErrorMessage(response);
  } catch (err) {
    console.log(err);
  }
};

// ボタンメッセージもしくはエラーメッセージを送る関数
const actionButtonOrErrorMessage = async (event: WebhookEvent) => {
  try {
    if (event.type !== 'message' || event.message.type !== 'text') {
      return;
    }

    const { replyToken } = event;
    const { text } = event.message;

    if (text === '今日の洋服は？') {
      const buttonMessage = await buttonMessageTemplate();
      await client.replyMessage(replyToken, buttonMessage);
    } else {
      // 「今日の洋服は？」以外のメッセージを受け取る
    }
  } catch (err) {
    console.log(err);
  }
};
```

ボタンメッセージのJSON作成に関しては公式サイトを参考にしましょう。

https://developers.line.biz/ja/reference/messaging-api/#buttons

### ③「今日の洋服は？」以外を受け取ったら、「そのメッセージには対応していません」と送る

```typescript:api/src/Common/ButtonMessage/ErrorMessageTemplate.ts
// パッケージを読み込む
import { TextMessage } from '@line/bot-sdk';

export const errorMessageTemplate = (): Promise<TextMessage> => {
  return new Promise((resolve, reject) => {
    const params: TextMessage = {
      type: 'text',
      text: 'ごめんなさい、このメッセージは対応していません。',
    };
    resolve(params);
  });
};
```

```typescript:api/src/index.ts
// パッケージのインストール
import { ClientConfig, Client, WebhookEvent } from '@line/bot-sdk';

// モジュールを読み込む
import { buttonMessageTemplate } from './Common/ButtonMessage/ButtonMessageTemplate';
import { errorMessageTemplate } from './Common/ButtonMessage/ErrorMessageTemplate';

// アクセストークンとチャンネルシークレットをenvから読み込む
const clientConfig: ClientConfig = {
  channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '',
  channelSecret: process.env.CHANNEL_SECRET || '',
};

// インスタンス化
const client: Client = new Client(clientConfig);

// 実行
exports.handler = async (event: any, context: any) => {
  const body: any = JSON.parse(event.body);
  const response: WebhookEvent = body.events[0];

  try {
    await actionButtonOrErrorMessage(response);
  } catch (err) {
    console.log(err);
  }
};

// ボタンメッセージもしくはエラーメッセージを送る関数
const actionButtonOrErrorMessage = async (event: WebhookEvent) => {
  try {
    if (event.type !== 'message' || event.message.type !== 'text') {
      return;
    }

    const { replyToken } = event;
    const { text } = event.message;

    if (text === '今日の洋服は？') {
      const buttonMessage = await buttonMessageTemplate();
      await client.replyMessage(replyToken, buttonMessage);
    } else {
      const errorMessage = await errorMessageTemplate();
      await client.replyMessage(replyToken, errorMessage);
    }
  } catch (err) {
    console.log(err);
  }
};
```

テキストメッセージのJSON作成に関しては公式サイトを参考にしましょう。

https://developers.line.biz/ja/reference/messaging-api/#text-message

### ④「位置情報メッセージ」を受け取る

```typescript:api/src/index.ts
// パッケージのインストール
import { ClientConfig, Client, WebhookEvent } from '@line/bot-sdk';

// モジュールを読み込む
import { buttonMessageTemplate } from './Common/ButtonMessage/ButtonMessageTemplate';
import { errorMessageTemplate } from './Common/ButtonMessage/ErrorMessageTemplate';
import { flexMessageTemplate } from './Common/WeatherForecastMessage/FlexMessageTemplate';

// アクセストークンとチャンネルシークレットをenvから読み込む
const clientConfig: ClientConfig = {
  channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '',
  channelSecret: process.env.CHANNEL_SECRET || '',
};

// インスタンス化
const client: Client = new Client(clientConfig);

// 実行
exports.handler = async (event: any, context: any) => {
  const body: any = JSON.parse(event.body);
  const response: WebhookEvent = body.events[0];

  try {
    await actionButtonOrErrorMessage(response);
    await actionFlexMessage(response);
  } catch (err) {
    console.log(err);
  }
};

// ボタンメッセージもしくはエラーメッセージを送る関数
const actionButtonOrErrorMessage = async (event: WebhookEvent) => {
  try {
    if (event.type !== 'message' || event.message.type !== 'text') {
      return;
    }

    const { replyToken } = event;
    const { text } = event.message;

    if (text === '今日の洋服は？') {
      const buttonMessage = await buttonMessageTemplate();
      await client.replyMessage(replyToken, buttonMessage);
    } else {
      const errorMessage = await errorMessageTemplate();
      await client.replyMessage(replyToken, errorMessage);
    }
  } catch (err) {
    console.log(err);
  }
};

const actionFlexMessage = async (event: WebhookEvent) => {
  try {
    if (event.type !== 'message' || event.message.type !== 'location') {
      return;
    }
    // 「位置情報メッセージ」を受け取る
  } catch (err) {
    console.log(err);
  }
};
```

### ⑤「位置情報メッセージ」を受け取ったら、緯度と経度を使って天気予報を取得する
Flex Messageの作成方法に関してファイル名も出しながら説明します。

#### 【ファイル名】GetWeatherForecast.ts
天気予報を取得します。

まずはOpenWeatherで天気予報を取得するために必要な情報が3つあります。
①API
②経度
③緯度
それではこの3つを取得していきましょう。

##### ①API
以下にアクセスしてください。

https://openweathermap.org/

アカウントを作成し、APIキーを発行してください。

![スクリーンショット 2021-05-29 20.28.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/a044d998-32d7-a132-c94b-6e55208b6819.png)

発行できたらこのAPIを`Lambdaの環境変数`に入力します。

![スクリーンショット 2021-07-14 16.46.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/c80091e1-f118-0d3a-5f7a-d0bf261a0e77.png)


##### ②経度、③緯度
これら2つは、eventから取得できます。

ということで作っていきましょう。

```typescript:api/src/Common/WeatherForecast/GetWeatherForecast.ts
// Load the package
import { WebhookEvent } from '@line/bot-sdk';
import axios, { AxiosResponse } from 'axios';

export const getWeatherForecastData = async (event: WebhookEvent): Promise<any> => {
  return new Promise(async (resolve, reject) => {
    try {
      if (event.type !== 'message' || event.message.type !== 'location') {
        return;
      }

      // Get latitude and longitude
      const latitude: number = event.message.latitude;
      const longitude: number = event.message.longitude;

      // OpenWeatherAPI
      const openWeatherAPI: string | undefined = process.env.WEATHER_API || '';

      // OpenWeatherURL
      const openWeatherURL: string = `https://api.openweathermap.org/data/2.5/onecall?lat=${latitude}&lon=${longitude}&units=metric&lang=ja&appid=${openWeatherAPI}`;

      const weatherData: AxiosResponse<any> = await axios.get(openWeatherURL);
      resolve(weatherData);
    } catch (err) {
      reject(err);
    }
  });
};
```

#### 【ファイル名】FormatWeatherForecast.ts
取得した天気予報のデータの整形を行う。

こちらでは、`const weather`と`const weatherArray`の2つで型定義ファイルを作成する必要があります。
ということで作成しましょう。

```typescript:api/src/Common/WeatherForecast/types/FormatWeatherForecast.type.ts
export type WeatherType = {
  dt: number;
  sunrise: number;
  sunset: number;
  moonrise: number;
  moonset: number;
  moon_phase: number;
  temp: {
    day: number;
    min: number;
    max: number;
    night: number;
    eve: number;
    morn: number;
  };
  feels_like: {
    day: number;
    night: number;
    eve: number;
    morn: number;
  };
  pressure: number;
  humidity: number;
  dew_point: number;
  wind_speed: number;
  wind_deg: number;
  wind_gust: number;
  weather: [
    {
      id: number;
      main: string;
      description: string;
      icon: string;
    }
  ];
  clouds: number;
  pop: number;
  rain: number;
  uvi: number;
};

export type WeatherArrayType = {
  today: string;
  imageURL: string;
  weatherForecast: string;
  mornTemperature: number;
  dayTemperature: number;
  eveTemperature: number;
  nightTemperature: number;
  fashionAdvice: string;
};
```

作成した型定義を使ってファイルを完成させます。

```typescript:api/src/Common/WeatherForecast/FormatWeatherForecast.ts
// Load the package
import { WebhookEvent } from '@line/bot-sdk';
import { AxiosResponse } from 'axios';

// Load the module
import { getWeatherForecastData } from './GetWeatherForecast';

// types
import { WeatherType, WeatherArrayType } from './types/FormatWeatherForecast.type';

export const formatWeatherForecastData = async (event: WebhookEvent): Promise<WeatherArrayType> => {
  return new Promise(async (resolve, reject) => {
    // Get the getWeatherForecastData
    const weathers: AxiosResponse<any> = await getWeatherForecastData(event);

    // Util
    const weather: WeatherType = weathers.data.daily[0];

    // Five required data
    // 1) Today's date
    const UNIXToday: number = weather.dt;
    const convertUNIXToday: Date = new Date(UNIXToday * 1000);
    const today: string = convertUNIXToday.toLocaleDateString('ja-JP');

    // 2) Weather forecast
    const weatherForecast: string = weather.weather[0].description;

    // 3) Temperature (morning, daytime, evening, night)
    const mornTemperature: number = weather.feels_like.morn;
    const dayTemperature: number = weather.feels_like.day;
    const eveTemperature: number = weather.feels_like.eve;
    const nightTemperature: number = weather.feels_like.night;

    // Bifurcate your clothing by maximum temperature
    const maximumTemperature: number = Math.max(
      mornTemperature,
      dayTemperature,
      eveTemperature,
      nightTemperature
    );

    // 4) Fashion Advice
    let fashionAdvice: string = '';

    // 5) Fashion Image
    let imageURL: string = '';

    if (maximumTemperature >= 26) {
      fashionAdvice =
        '暑い！半袖が活躍する時期です。少し歩くだけで汗ばむ気温なので半袖1枚で大丈夫です。ハットや日焼け止めなどの対策もしましょう';
      imageURL =
        'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/60aa3c44153071e6df530eb7_71.png';
    } else if (maximumTemperature >= 21) {
      fashionAdvice =
        '半袖と長袖の分かれ目の気温です。日差しのある日は半袖を、曇りや雨で日差しがない日は長袖がおすすめです。この気温では、半袖の上にライトアウターなどを着ていつでも脱げるようにしておくといいですね！';
      imageURL =
        'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056e58a5923ad81f73ac747_10.png';
    } else if (maximumTemperature >= 16) {
      fashionAdvice =
        'レイヤードスタイルが楽しめる気温です。ちょっと肌寒いかな？というくらいの過ごしやすい時期なので目一杯ファッションを楽しみましょう！日中と朝晩で気温差が激しいので羽織ものを持つことを前提としたコーディネートがおすすめです。';
      imageURL =
        'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6087da411a3ce013f3ddcd42_66.png';
    } else if (maximumTemperature >= 12) {
      fashionAdvice =
        'じわじわと寒さを感じる気温です。ライトアウターやニットやパーカーなどが活躍します。この時期は急に暑さをぶり返すことも多いのでこのLINEで毎日天気を確認してくださいね！';
      imageURL =
        'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056e498e7d26507413fd853_4.png';
    } else if (maximumTemperature >= 7) {
      fashionAdvice =
        'そろそろ冬本番です。冬服の上にアウターを羽織ってちょうどいいくらいです。ただし室内は暖房が効いていることが多いので脱ぎ着しやすいコーディネートがおすすめです！';
      imageURL =
        'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056e4de7156326ff560b1a1_6.png';
    } else {
      fashionAdvice =
        '凍えるほどの寒さです。しっかり厚着して、マフラーや手袋、ニット帽などの冬小物もうまく使って防寒対策をしましょう！';
      imageURL =
        'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056ebd3ea0ff76dfc900633_48.png';
    }

    // Make an array of the above required items.
    const weatherArray: WeatherArrayType = {
      today,
      imageURL,
      weatherForecast,
      mornTemperature,
      dayTemperature,
      eveTemperature,
      nightTemperature,
      fashionAdvice,
    };

    resolve(weatherArray);
  });
};
```

#### 【ファイル名】FlexMessageTemplate
整形したデータを取得して Flex Messageのテンプレートを作成する。

```typescript:api/src/Common/WeatherForecast/FlexMessageTemplate.ts
// Load the package
import { WebhookEvent, FlexMessage } from '@line/bot-sdk';

// Load the module
import { formatWeatherForecastData } from './FormatWeatherForecast';

export const flexMessageTemplate = async (event: WebhookEvent): Promise<FlexMessage> => {
  return new Promise(async (resolve, reject) => {
    const data = await formatWeatherForecastData(event);

    resolve({
      type: 'flex',
      altText: '天気予報です',
      contents: {
        type: 'bubble',
        header: {
          type: 'box',
          layout: 'vertical',
          contents: [
            {
              type: 'text',
              text: data.today,
              color: '#FFFFFF',
              align: 'center',
              weight: 'bold',
            },
          ],
        },
        hero: {
          type: 'image',
          url: data.imageURL,
          size: 'full',
        },
        body: {
          type: 'box',
          layout: 'vertical',
          contents: [
            {
              type: 'text',
              text: `天気は、「${data.weatherForecast}」です`,
              weight: 'bold',
              align: 'center',
            },
            {
              type: 'text',
              text: '■体感気温',
              margin: 'lg',
            },
            {
              type: 'text',
              text: `朝：${data.mornTemperature}℃`,
              margin: 'sm',
              size: 'sm',
              color: '#C8BD16',
            },
            {
              type: 'text',
              text: `日中：${data.dayTemperature}℃`,
              margin: 'sm',
              size: 'sm',
              color: '#789BC0',
            },
            {
              type: 'text',
              text: `夕方：${data.eveTemperature}℃`,
              margin: 'sm',
              size: 'sm',
              color: '#091C43',
            },
            {
              type: 'text',
              text: `夜：${data.nightTemperature}℃`,
              margin: 'sm',
              size: 'sm',
              color: '#004032',
            },
            {
              type: 'separator',
              margin: 'xl',
            },
            {
              type: 'text',
              text: '■洋服アドバイス',
              margin: 'xl',
            },
            {
              type: 'text',
              text: data.fashionAdvice,
              margin: 'sm',
              wrap: true,
              size: 'xs',
            },
          ],
        },
        styles: {
          header: {
            backgroundColor: '#00B900',
          },
          hero: {
            separator: false,
          },
        },
      },
    });
  });
};
```

### ⑥「位置情報メッセージ」を受け取ったら、天気予報メッセージを送る

```typescript:api/src/index.ts
// パッケージのインストール
import { ClientConfig, Client, WebhookEvent } from '@line/bot-sdk';

// モジュールを読み込む
import { buttonMessageTemplate } from './Common/ButtonMessage/ButtonMessageTemplate';
import { errorMessageTemplate } from './Common/ButtonMessage/ErrorMessageTemplate';
import { flexMessageTemplate } from './Common/WeatherForecastMessage/FlexMessageTemplate';

// アクセストークンとチャンネルシークレットをenvから読み込む
const clientConfig: ClientConfig = {
  channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '',
  channelSecret: process.env.CHANNEL_SECRET || '',
};

// インスタンス化
const client: Client = new Client(clientConfig);

// 実行
exports.handler = async (event: any, context: any) => {
  const body: any = JSON.parse(event.body);
  const response: WebhookEvent = body.events[0];

  try {
    await actionButtonOrErrorMessage(response);
    await actionFlexMessage(response);
  } catch (err) {
    console.log(err);
  }
};

// ボタンメッセージもしくはエラーメッセージを送る関数
const actionButtonOrErrorMessage = async (event: WebhookEvent) => {
  try {
    if (event.type !== 'message' || event.message.type !== 'text') {
      return;
    }

    const { replyToken } = event;
    const { text } = event.message;

    if (text === '今日の洋服は？') {
      const buttonMessage = await buttonMessageTemplate();
      await client.replyMessage(replyToken, buttonMessage);
    } else {
      const errorMessage = await errorMessageTemplate();
      await client.replyMessage(replyToken, errorMessage);
    }
  } catch (err) {
    console.log(err);
  }
};

const actionFlexMessage = async (event: WebhookEvent) => {
  try {
    if (event.type !== 'message' || event.message.type !== 'location') {
      return;
    }

    const { replyToken } = event;
    const message = await flexMessageTemplate(event);

    await client.replyMessage(replyToken, message);
  } catch (err) {
    console.log(err);
  }
};
```

これで完成です！
`npm run build`でコンパイルしましょう。

```terminal:ターミナル
$ npm run build
```

## S3にアップロードしましょう
コンパイルすると`dist`ディレクトリに2つのファイルと1つのフォルダがあるかと思います。

| ファイル     | フォルダ |
| ------------ | -------- |
| index.js     | Common   |
| index.js.map |          |

これら3つと`node_modules`でzipを作成しましょう。

![スクリーンショット 2021-07-14 16.55.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/ea078702-ca63-776d-08fb-f8102bb097a3.png)

私は`__dist.zip`という名前で作成しました。

ちなみになぜnode_modulesが必要になるかなのですが、
現在Lambda関数内でLINEのパッケージが使える状況にありません。
なのでLambda関数内で使えるようにするために、`node_modules`をLambdaに渡す必要があるのです。

アップロードはめちゃくちゃ簡単です。
ただドラッグアンドドロップするだけです。

![スクリーンショット 2021-07-14 16.58.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/6cfaa8f2-278e-eadd-2936-250b44bb1a7e.png)

アップロードが完了すると**オブジェクトURL**が設定されるのでこれをLambdaにアップロードします。

![スクリーンショット 2021-07-14 17.02.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/0551e7f7-9e9f-7e74-6565-21015e1d8005.png)

これで完成です！

## 最後に
今回はすべて手作業で行いました。
所々デバッグしながら進めていきたいですが、
そのためには毎回S3にアップしてそれをLambdaでもアップするという作業が必要になります。

こんなのめんどくさいですよねw

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/6753c6f6-5f29-24b4-96d4-1d24b4644d36.png)

ということで、そんな人のためにSAMというものがあります。

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/what-is-sam.html

SAMを使えば、ローカルでデバッグやテストができるようになるとともに、
コマンド1つでデプロイすることが可能になります。

次回は今回作ったアプリをSAMを使って作りたいと思います。