LINE BotサーバーをNestJSで開発する その2

[前回](https://qiita.com/bathtimefish/items/e6c151632ad1e9ca71ed)の続きです。立ち上げたエコーサーバーのコードの要点をざっと見ていきます。

ソースコードはこちらです。
https://github.com/bathtimefish/line-botserver-example-nestjs/tree/v1

# main.ts
```src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as fs from 'fs';
import { config } from 'dotenv';

function getOptions() {
  return {
    httpsOptions: {
      key: fs.readFileSync(process.env.SSL_KEY_PATH),
      cert: fs.readFileSync(process.env.SSL_CERT_PATH),
    },
  };
}

async function bootstrap() {
  let options = {};
  if (process.env.SSL_KEY_PATH && process.env.SSL_CERT_PATH) {
    options = getOptions();
  }
  const app = await NestFactory.create(AppModule, options);
  await app.listen(3000);
}
config();
bootstrap();
```

ここではデフォルトで生成されたコードにhttpsサーバー化の設定を追加しています。LINE PlatformからのWebhookを受けるためにはhttpsサーバーである必要があるからです。

`NestFactory.create(AppModule, options);`でサーバーのインスタンスを作成し、`app.listen(3000);`でポート3000でBotサーバーを起動しています。`AppModule`はBotサーバーを構成するモジュールで後述します。第二引数の`options`には`getOptions()`で取得したLet's Encryptの証明書ファイルのパス設定が含まれています。NestJSでは[httpsOptions](https://docs.nestjs.com/faq/multiple-servers)に証明書のパスを設定することでWebサーバーをhttpsサーバーとして作成することができます。

# app.module.ts

```src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MessagingModule } from './messaging/messaging.module';

@Module({
  imports: [MessagingModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
```

`@Module()`デコレーターで注釈されるNestJSの[Module](https://docs.nestjs.com/modules)このModuleによってサーバーアプリケーションの構造が簡潔に定義できるようになっています。ここでは`imports`にMessagingAPIを扱うModuleをインポートしています。`imports`は他のModuleをimportして本Moduleに統合することができるので複数のModuleを構造的に開発することができます。

`controllers`にはこのModuleで動作させるControllerのリストを設定します。Controllerは`@Controller()`で注釈されるリクエスト/レスポンスを制御する機能を定義します。`providers`にはController等で共通的に使われる機能を`@Injectable()`によって注釈されるクラスにより提供します。

上記コードの`controllers`と`providers`はNestJSの初期プロジェクト設定時のままで変更しておらず、`imports: [MessagingModule],`を追加したのみです。つまり今回カスタマイズしたLIME Messaging APIのエコーサーバーの全機能は`MessagingModule`に入っていることになります。では、`MessagingModule`を見ていきましょう。

# messaging.module.ts

```src/messaging/messaging.module.ts
import { Module } from '@nestjs/common';
import { MessagingController } from './messaging.controller';
import { MessagingService } from './messaging.service';

@Module({
  controllers: [MessagingController],
  providers: [MessagingService],
  exports: [MessagingService],
})
export class MessagingModule {}
```

今回カスタマイズした部分はほぼ[src/messaging](https://github.com/bathtimefish/line-botserver-example-nestjs/tree/v1/src/messaging)に収まっています。それ以外は上記の`main.ts`をhttpsの設定を加えただけです。

`messaging.module.ts`はNestJSのModuleなので`app.module.ts`の構造と全く同じです。異なる点は、このコードでは`imports`を使用しておらず`exports`で読み込んだプロバイダを公開しています。

# messaging.controller.ts

```src/messaging/messaging.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { LineMessagingApiDto } from './messaging.dto';
import { MessagingService } from './messaging.service';

@Controller('messaging')
export class MessagingController {
  constructor(private readonly messagingService: MessagingService) {}
  @Get()
  getMessages() {
    return this.messagingService.getMessage();
  }
  @Post()
  postMessages(@Body() body: LineMessagingApiDto) {
    return this.messagingService.dispachMessage(body);
  }
}
```

MessagingAPIのロジックを定義するControllerです。`@Controller('messaging')`によって、APIのパスが`/messaging`となります。リクエストメソッドごとに定義されるクラスメソッドは`@Get()`、`@Post()`といったアノテーションによって非常に簡潔に定義できます。各メソッド内の機能は後述する`messaging.service.ts`内のProviderで提供されています。ProviderはControllerのクラスに注入されているため`this.messagingService.getMessage();`のように自クラスの機能のように利用できます。

# messaging.service.ts

```src/messaging/messaging.service.ts
import { Injectable } from '@nestjs/common';
import type { ILineMessagingApi, ISimpleMessage } from './messaging.interface';
import { messagingApi } from '@line/bot-sdk';
import type { ClientConfig } from '@line/bot-sdk';

const getLineBotClient = () => {
  const clientConfig: ClientConfig = {
    channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN,
    channelSecret: process.env.CHANNEL_SECRET,
  };
  return new messagingApi.MessagingApiClient(clientConfig);
};

@Injectable()
export class MessagingService {
  getMessage(): ISimpleMessage {
    return { message: 'This is a LINE Bot Server powered by NestJS' };
  }
  async dispachMessage(body: ILineMessagingApi): Promise<void> {
    const client = getLineBotClient();
    const event = body.events[0];
    let textMessage: { type: string; text: string };
    switch (event.message.type) {
      case 'text':
        textMessage = {
          type: 'text',
          text: event.message.text,
        };
        break;
      default:
        textMessage = {
          type: 'text',
          text: 'Server receinved an invalid message type',
        };
    }
    await client.replyMessage({
      replyToken: event.replyToken,
      messages: [textMessage],
    });
  }
}
```

`messaging.service.ts`にはMessaging APIを利用するエコーサーバーのロジックを含むメソッドが記述されています。ここに定義されている`getMessage()`や`dispatchMessage()`をController側に記述することもできますが、上記のようにProviderとして提供することで、`messaging.controller.ts`以外のControllerでこれらの機能を利用したくなった場合にすぐ提供することができます。

`dispachMessage()`がLINE PlatformからWebhookを受信したときに動作するメソッドです。ここでは簡単のためTextEventMessageを受信したときのみ、テキストの内容をそのまま返信するようにしています。

```src/messaging/messaging.service.ts
await client.replyMessage({
  replyToken: event.replyToken,
  messages: [textMessage],
});
```

で、Message Eventに含まれる`replyToken`とテキストメッセージを設定してメッセージを送信したクライアントに対してメッセージを返信しています。

# おわりに

NestJSで作成したシンプルなLINE Botサーバーのコードをざっと見ていきました。NestJSは大規模なWebサーバーを効率よく開発/メンテナンスするために大変優れた構造を持っています。LINE Botを開発保守している上で、NestJSは良い選択肢の一つになるんじゃないでしょうか。