Discordに画像を投稿するBOT #1準備編

以前[Weakened Fun Time](https://weakenedfuntimeblog.wordpress.com/)で作っていた[スクリーンショットをDiscordに投稿するボット](https://github.com/WeakenedPlayer/ns-screenshot-bot)は、大分前にメンテナンスを放棄してしまった。というのも、以前のバージョンはコマンドライン上で動かすので使い勝手が悪く、かといってElectronで作るのも大変だしメモリを食うし微妙になってしまったためだ。

なので最近のWebアプリ風に、ブラウザから設定できるようにしようと思う。こうすれば、スマホやタブレット上で操作できるようになり、ゲーム中でも操作できるようになる。

#やろうとしてること

* スクリーンショットフォルダに追加された画像をDiscordに投稿する
* 画像はPNG→JPGに変換する(FullHDだと5MBくらいなのでDiscordのレートリミットにすぐ引っかかる)
* 投稿先のサーバ・チャネルを設定できる
* 投稿のON/OFFを切り替えられる
* ゲーム中でも設定できる(スマホやタブレットから設定できる)

#準備
最初に空のプロジェクトを立ち上げることから始める。設定画面を作るAngularのプロジェクトを`client`に作成し、設定画面をブラウザに送るサNestJSサーバのプロジェクトを`server`に作成する。

```
npm install -g @angular/cli
ng new client

npm install -g @nestjs/cli
nest new server
```

#ブラウザでAngularの画面を表示する
まず、クライアント側の設定を変更する。`client/angular.json`の`outputPath`を変更し、Angularプロジェクトのビルド結果を、`server/public`以下に置くように設定変更する。

これをサーバで表示できるように、`server/main.ts`で静的ファイルのパスを設定する。

```typescript:server/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { join } from 'path';

async function bootstrap() {
    const app = await NestFactory.create( AppModule );
    
    app.useStaticAssets( join( __dirname, '..', 'public') );    // server/public にバンドルしたファイルを置く

    await app.listen( 3000 );   // TODO: コマンドライン引数でポートを変えられるようにする
}
bootstrap();
```

この変更後、以下コマンドでサーバを立ち上げる。

```
npm run start
または
npm run start:dev
```

そして自PCから`localhost:3000`を開くか、LAN内のPCから`<IP>:3000`を開くかすると、以下のような画面が現れるはずである。これで、基本的な準備が完了する。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/374475/17092ae8-738a-8816-809e-fae016fc37d6.png)

#ブラウザからDiscordのBOTを操作する
試しに、DiscordのBotを操作する簡単なREST APIを設けてみる。ここまでに用意したAngular画面にボタンやテキストボックスを配置し、そこからNestJSで用意したREST APIを操作できるようにしてみる。

[コードはGitHub参照](https://github.com/WeakenedPlayer/botty/releases/tag/0.0.3)。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/374475/1b0791f2-4bfe-381e-1180-535358ff535d.png)

##バックエンド抜粋: コントローラ
とりあえず簡易REST APIで操作できるよう、`server`と`message`という2つのエンドポイントを用意する。`server`では、リクエストボディの`active`プロパティの値(`true`か`false`)で、接続・切断を切り替えられるようにする。`message`では、`message`プロパティの値をそのままDiscordのボットに送信する。ボットや送信先のチャネルは`config.ts`内で定義してある。(GitHubには入れていない)

```typescript:コントローラ
import { Controller, Get, Body, Res, Post, Patch } from '@nestjs/common';
import { ApiModelProperty, ApiResponse } from '@nestjs/swagger';

import { DiscordBotService } from './service'

export class MessageDto {
    @ApiModelProperty( { required: true } )
    message: string;
}

export class ServerStatusDto {
    @ApiModelProperty( { required: true } )
    active: boolean;
}

@Controller()
export class DiscordBotController {
    constructor( private bot$: DiscordBotService ) {}
    
    @Post('message')
    async message( @Body() message: MessageDto ) {
        await this.bot$.send( message.message );
    }
    
    @Patch('server')
    async connect( @Body() serverStatus: ServerStatusDto ) {
        if( serverStatus.active ) {
            await this.bot$.connect();
        } else {
            await this.bot$.disconnect();
        }
    }   
}
```

##フロントエンド抜粋:操作画面
上記のバックエンドの2つのエンドポイントを操作するメソッドを用意し、簡易画面から操作できるようにする。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/374475/cbd821d1-3c2a-6172-ad7c-d31df66a611e.png)

```typescript:Angularコンポーネント
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component( {
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.scss']
} )
export class AppComponent {
    message: string = '';

    constructor( private http$: HttpClient ) {}
    
    async connect() {
        await this.http$.patch( '/server', { "active": true } ).toPromise();
    }
    
    async disconnect() {
        await this.http$.patch( '/server', { "active": false } ).toPromise();
    }
    
    async send() {
        await this.http$.post( '/message', { "message": this.message } ).toPromise();
        this.message = '';
    }
}
```

##動作イメージ
コード: https://github.com/WeakenedPlayer/botty/releases/tag/0.0.3
[![動作イメージ](http://img.youtube.com/vi/9ISuP_AEuS0/0.jpg)
](https://www.youtube.com/watch?v=9ISuP_AEuS0)