チュートリアル
https://gyazo.com/f834743d34d2746d7e4a6bbd02fc44b2
Quro を使った簡単なボットをチュートリアルです。
なお言語はTypeScriptを使用します。
今回作成するボットはGitHubで公開されています。
https://github.com/quro-framework/quro-tutorial-bot
必要環境/用意するもの
Node.js >= v14.0.0
Discordボットのトークン
エディタ
今回作成するボット
今回作るボットはusernameコマンドとreverseコマンドの2つのコマンドを持ったシンプルなボットです。
各コマンドの説明は以下のとおりです。
username
メッセージを作成したユーザー名を返すコマンドです。
パイプとしてメッセージを作成したユーザー名を文字列で渡します。
reverse
与えられた文字列を反転して返すコマンドです。
下の画像は今回のチュートリアルで作成するボットの画像です。
https://gyazo.com/1ed766dd3d00a49a8a76af0fd9917131
ステップ0. quro-cliをインストールしよう
Quroを使ったプロジェクトを作るには quro-cli を使うのがおすすめです。
今回はこのquro-cliを使ってプロジェクトを準備していきます。
以下のコマンドでquro-cliをインストールしてください。
code:bash
npm install -g @quro/cli
# or
yarn global add @quro/cli
quro --help でコマンドを確認できます。
bot
新しくボットを作成します。
command
新しくコマンドを作成します。
https://gyazo.com/aa4fe0dee87ccb2409eba91e0fa70f0e
ステップ1. ボットを作成しよう
以下のコマンドでボットが作成できます。quro-tutorial の部分はボット名なので好きに変更してOKです。
code:bash
quro bot quro-tutorial
実行するとボットの情報を入力するプロンプトが表示されます。(実行時にWarning: Accessing non-existent property 'toEnd'...という警告が表示される場合がありますが気にしなくて大丈夫です。この問題は現在調査中です。)
プロンプトでは基本 Enter 連打で大丈夫です。
https://gyazo.com/dcaca802580e28cd2668a41a36f41b95
各項目の詳細は以下のとおりです。
table:各項目の詳細
npm package name ボットの名前です。
╰──────────────── quro bot <ボット名> で指定された名前がデフォルトです。
description ボットの説明です。
author ボットの所有者名です。
version ボットのバージョンです。
prefix ボットのプレフィックスです。$ がデフォルトです。
Who is the GitHub owner of repository ボットのGithub上のリポジトリの所有者名です。
What is the GitHub name of repository ボットのGithub上のリポジトリ名です。
Select a package manager パッケージマネージャーの選択です。
╰──────────────── npm か yarn が選べます。quro の依存パッケージをインストールする際に利用します。
Use dotenv dotenvを利用するかどうかです。
╰──────────────── yes にした場合ボットのトークンをdotenvで読み込めるようになります。
TypeScript TypeScriptを利用するかどうかです。
Use eslint eslint を利用するかどうかです。TypeScript が有効な場合、
╰──────────────── TypeScript 向けのeslintパッケージがインストールされます。
Use prettier prettier を利用するかどうかです。
Use nodemon nodemonを利用するかどうかです。
╰──────────────── yes にするとボットのソースコードが変更されるとボットが自動で再起動します。
ステップ2. 構成の確認
先程の項目が入力し終わると、コマンドが実行されたディレクトリに <ボット名> というディレクトリが作成されます。
ディレクトリの構成の詳細は以下のとおりです。
code:bash
quro-tutorial ──────────────── プロジェクトディレクトリ
├── node_modules/
├── LICENSE
├── README.md
├── nodemon.json
├── package.json
├── src ────────────────────── ボットのソースコードのディレクトリ
│   ├── commands/ ──────────── ボットのコマンドを置くディレクトリ
│   └── index.ts ──────────── ボットのエントリーポイント
├── .env ───────────────────── dotenvで読み込まれるファイル
├── tsconfig.json
└── yarn.lock
ステップ3. トークンを.envに記述しよう
まずディレクトリ内での .env ファイルをエディタで開いてください。
このNO_TOKENを用意したボットのトークンに書き換えてください。
code:.env
DISCORD_BOT_TOKEN=<ボットのトークン>
ステップ4. watchモードで起動しよう
cd でボットのディレクトリに移動し、yarn または npm でwatchスクリプトを起動してください。
code:bash
cd <ボットディレクトリ>
yarn watch
# or npm run watch
ボットがオンラインになっていれば成功です。
https://gyazo.com/0cf4ac01feda279f58762cda80524239
この状態で $ping と打つとボットが Pong! と返信してくるはずです。
https://gyazo.com/57512d234ac7d0b7a8c1e3a6d6ebb86b
ステップ5. src/index.tsの確認
このステップでは src/index.ts ファイルのコードの確認をします。
このファイルはボットを起動するためのコードが書かれています。
少し長いですが、以下のコードは src/index.ts に各コードの解説コメントを付けたものです。
code:src/index.ts
import { QuroBot, CommandFileLoader } from 'quro'
import * as dotenv from 'dotenv'
import * as path from 'path'
import { version } from '../package.json'
dotenv.config() // .envの読み込み
class Bot extends QuroBot {
prefixes = '$' // プレフィックスの配列
version = version // ボットのバージョンの文字列
/**
* Setup.
*/
async setup() {
await this.registerDirectoryCommands('./commands') // src/commands フォルダ内にあるコマンドをボットに登録
}
/**
* Register directory commands.
*
* @param directoryPath
*/
private async registerDirectoryCommands(directoryPath: string) {
const commandLoader = new CommandFileLoader() // ディレクトリのファイルからコマンドを読み込むクラス
// registerCommands でコマンドを登録
this.registerCommands(
await commandLoader.load(path.resolve(__dirname, directoryPath))
)
}
}
const bot = new Bot() // ボットを生成
bot.start(process.env.DISCORD_BOT_TOKEN) // ボットを起動
ステップ6. src/commands/Ping.tsの確認
このファイルは「ステップ4. watchモードで起動しよう」で実行した $ping コマンドが実装されているファイルです。
他のコマンドを実装する際もこのファイルと同じような構造になります。
少し長いですが、以下のコードは src/commands/Ping.ts に各コードの解説コメントを付けたものです。
code:src/commands/Ping.ts
import { Command, CommandRequest } from 'quro'
export class PingCommand extends Command { // Command クラスを継承して PingCommand を定義
name = 'ping' // コマンドの名前
aliases = [] // コマンドのエイリアスの配列
description = '' // コマンドの説明
argDefs = {} // コマンドの引数の定義
/**
* Call on handle.
*
* @param request
*/
onHandle(request: CommandRequest) { // コマンドの実行処理
// メッセージに 'Pong!' とリプライ
request.message.reply('Pong!') // request.message はDiscord.js の Message オブジェクト
}
/**
* Returns parsed arguments.
*
* @param request
*/
getArgs(request: CommandRequest) { // argDefs を元に引数のオブジェクトを生成するメソッド
return this.parseArgs<PingCommand>(request)
}
}
export const ping = new PingCommand() // コマンドのインスタンスをエクスポート(この行がないと CommandFileLoader が反応しない)
ステップ7. username コマンドの実装
このステップでは $username と送られると、メッセージを送ったユーザーのユーザー名を返すコマンドを実装します。
また username コマンドのパイプとして、メッセージを送ったユーザーのユーザー名を渡す機能も実装します。
https://gyazo.com/ce589f00a4e6e69b2fe6e5ad211e0578
「ステップ6」であった Ping コマンドのソースコードはかなりの行数がありました。あれを書くのは面倒ですし、コピーするのもなんだかなぁです。
なのでコマンドの作成には quro command <コマンド名> を使うのをおすすめします。
このコマンドはsrc/commands/<コマンド名>.tsをコマンドの雛形から生成してくれます。
早速 username コマンドを以下のコマンドで生成してみましょう。
code:bash
quro command username
src/commands/Username.tsが作成されていたら成功です。
生成されたファイルを開くと以下のようなコードが記述されています。
code:src/commands/Username.ts
import { Command, CommandRequest } from 'quro'
export class UsernameCommand extends Command {
name = 'username'
aliases = []
description = ''
argDefs = {}
/**
* Call on handle.
*
* @param request
*/
onHandle(request: CommandRequest) {
const args = this.getArgs(request)
// Handle process here.
}
/**
* Returns parsed arguments.
*
* @param request
*/
getArgs(request: CommandRequest) {
return this.parseArgs<UsernameCommand>(request)
}
}
export const username = new UsernameCommand()
コマンドの処理を書くのは onHandle メソッド内なので、早速ユーザー名を返すコードを記述しましょう。
以下のコードを onHandle メソッドに記述してください。
code:src/commands/Username.ts
/**
* Call on handle.
*
* @param request
*/
onHandle(request: CommandRequest) {
return this.reply(request.author.username)
}
このコードを詳しく説明していきます。
まず onHandle(request: CommandRequest) は onHandle メソッドの定義です。
引数として CommandRequest 型の request を受け取ります。
この CommandRequest 型はユーザーが作成したメッセージからコマンドや引数やパイプ演算子などをパースし、オブジェクト化したものです。
次に return this.reply(...) ですが、これは ReplyResponse を生成するためのメソッドです。
ReplyResponse とはDiscord.jsの Message#reply を使用して、コンテンツを返信するためのレスポンスクラスです。
return this.reply(...) の ... には文字列やRich Embedなどを渡すことができます。
最後に request.author.username です。この request.author というのはメッセージを作成したユーザーの Discord.js の User オブジェクトです。
このように Quro のコマンドは
リクエスト→コマンド→レスポンスのように処理されます。
なぜこのような構造になっているかというと、ボットの開発者がなるべく他のことを意識せずに少ないコードで様々な処理や機能を実装できるようにするためです。
以下の(雑な)図はメッセージの作成からボットの応答までを軽くまとめた処理の流れです。クリックすると拡大できます。
https://gyazo.com/24296f23f632128772c725bc79cc25f7
さて、次はパイプの実装です。
今回の username コマンドはパイプとしてメッセージを作成したユーザーのユーザー名を渡すという機能も実装します。
まず、パイプを取り扱うために PipeNext という型を import する必要があります。
src/commands/Username.ts ファイルの最初の行を以下のように変更します。
code:src/commands/Username.ts
import { Command, CommandRequest, PipeNext } from 'quro'
// -----------------------------------↑ココ
/* 省略 */
次にUsernameCommandクラスに onPipe というメソッドを定義します。
位置はonHandleメソッドの下にしておきましょう。
code:src/commands/Username.ts
onHandle(request: CommandRequest) {/* 省略 */}
// onHandleメソッドの下に以下を追記
onPipe(request: CommandRequest, next: PipeNext) {
return next.setAppendArgs(request.author.username)
}
このようにコマンドに onPipe というメソッドを定義するとそのコマンドは パイプ演算子(>) で次のコマンドへデータを渡すことができます。
データを渡すというのは正確に言うと、次のコマンドへ追加の引数を追加すること。
それでは各コードを解説していきます。
onPipe(request: CommandRequest, next: PipeNext) は onPipe メソッドの定義です。
引数として CommandRequest 型のrequest と PipeNext 型の next を受け取ります。
CommandRequest 型は先程解説したので、PipeNext 型について解説します。
PipeNext は次のコマンドへ渡す引数を設定するためのオブジェクトです。
next.setAppendArgs([...args]) は次のコマンドの引数の末尾に引数を追加するための関数です。引数の値を配列で指定します。
また next.setPrependArgs([...args]) は次のコマンドの引数の一番前にに引数を挿入するための関数です。
onPipe メソッドはこの next.setAppendArgs 関数または next.setPrependArgs 関数の返り値を返す必要があります。
今回はreturn next.setAppendArgs([request.author.username]) としてユーザー名を引数として追加しています。
ではコマンドを実際に実行してみましょう。今回は nodemon により、ボットの再起動が自動で行われているため、そのままDiscordで送ってしまいましょう。
$username と送ると自分のユーザー名が返ってくるはずです。
https://gyazo.com/ce589f00a4e6e69b2fe6e5ad211e0578
これで username コマンドの実装は完了です。
ステップ8. reverse コマンドを実装しよう
reverse コマンドは引数として与えられた文字列を反転(reverse)して返します。username コマンドとは違い、パイプは実装しないので、ササッと実装してしまいましょう。
username コマンドと同じように quro command コマンドでreverse コマンドを生成します。
以下のコマンドを実行してください。
code:bash
quro command reverse
src/commands/Reverse.tsファイルが作成されていたら成功です。
reverse コマンドは引数を受け取るため、引数の情報を定義してあげる必要があります。
引数の定義には
引数の名前
型
などが必要です。
今回のreverseコマンドは引数として対象の文字列を受け取ります。なので以下のようになります。
table:reverseコマンドの引数
引数の名前 input
引数の型 String
そしてコマンドの引数を定義するにはコマンドのクラスのargDefsオブジェクトを使用します。
早速reverseコマンドの引数を定義していきましょう。
まず引数を定義するための ArgDef クラスとArgType型をimportする必要があります。
src/commands/Reverse.ts の1行目を以下のように変更してください。
code:src/commands/Reverse.ts
import { Command, CommandRequest, ArgDef, ArgType } from 'quro'
// ---------------------------------↑ココ----↑ココ
/* 省略 */
次に src/commands/Reverse.ts の10行目あたりを以下のように変更してください。
code:src/commands/Reverse.ts
export class ReverseCommand extends Command {
/* 省略 */
argDefs = {
input: new ArgDef({
name: 'input',
type: ArgType.String,
}),
}
/*省略*/
}
このようにargDefsオブジェクトに
<引数名>: new ArgDef({ name: '<引数名>', type: <引数の型> })
と記述することで、引数を定義することができます。
<引数の型> には ArgTypeの値を指定します。今回は文字列型の引数なのでArgType.Stringを指定しました
ArgTypeには他にも以下の様な型があります。
Any
全ての値を許容する型
String
文字列型
Number
数値型
Boolean
ブーリアン型
最後にonHandleメソッドを実装します。
src/commands/Reverse.ts のonHandleメソッドを以下の様に変更してください。
code:src/commands/Reverse.ts
onHandle(request: CommandRequest) {
const args = this.getArgs(request)
const reversed = args.input.split('').reverse().join('')
return this.reply(reversed)
}
では各コードの解説をしていきます。
まずconst args = this.getArgs(request)です。
これはCommandRequestオブジェクトからReverseコマンドの引数からTypeScriptで補完可能な引数のオブジェクトを返します。
つまり args は以下の様な構造になっています。
code:typescript
const args: {
input: string
}
このstringというのはArgTypeで指定された型の種類によって変わります。
次に
const reversed = args.input.split('').reverse().join('')
では args.input を反転してreversed 変数に代入しています。
最後に return this.reply(reversed) で結果を返します。
それでは早速 reverse コマンドを実行してみましょう。
$reverse Helloと送ってみましょう。
https://gyazo.com/72632d7b6984f121b1e4cd2307b49b4e
このように olleH と返って来たら成功です。
次は試しに $reverse Hello world と送ってみましょう。
https://gyazo.com/6b6f41202b01413416e58e3346acf1f6
結果は返ってきましたが、dlrow olleH ではなく先ほどと同じ olleH が返ってきました。
これは Hello world が Hello と world といった具合に、2つの引数として解釈されているためです。
これを回避する方法としては空白を\ のようにバックスラッシュ + 空白として表現することです。
試しに$reverse Hello\ worldと送ってください。
https://gyazo.com/a817a0552329e0405ce22025ba4060b3
ちゃんと返ってきていますね。
他にもHello worldを"Hello world"としてダブルクォーテーションかシングルクォーテーションで囲む方法です。
しかし、この様な方法を毎回取るのは面倒臭いです。
この様な問題は useFirstArgValue というメンバ変数をコマンドのクラスに定義しtrueを代入することで回避できます。
code:src/commands/Reverse.ts
export class ReverseCommand extends Command {
/* 省略 */
useFirstArgValue = true
/* 省略 */
}
この useFirstArgValue は
onPipeを持たないコマンドかつ
引数の数が1つかつ
引数の型がArgType.String
のコマンドに限り、使うことができます。
この useFirstArgValue が true に設定されている場合、argument-parser は空白が含まれている文字列も1つの引数として解釈してCommandRequestに引数を設定します。
実際に$reverse Hello world を実行してみましょう。
https://gyazo.com/e7bea1d9419ec65bca33af2959638f08
ちゃんと dlrow olleH が返ってきています。
しかし、今回は onPipe を持つコマンドのため、useFirstArgValue = true の行は削除しましょう。
code:src/commands/Reverse.ts
export class ReverseCommand extends Command {
/* 省略 */
/* 省略 */
}
なぜパイプのコマンドが useFirstArgValue の対象外なのかは以下の様な理由があります。
パイプのコマンドが、複数の引数を受け取るのを前提としているため
useFirstArgValue = trueなコマンドのパイプは正常に動作しないため
ステップ9. パイプを試してみよう
ここまでお疲れさまです。
もう実際にパイプの機能は実装したので、早速試してみましょう。
$username > reverse と送ってみましょう。
https://gyazo.com/a696a83fa8605cb34d67cc04432bb0b7
このように自分のユーザー名が反転して返ってきたら成功です。
ここまでお疲れさまでした
これで今回のチュートリアルは完了です。お疲れさまでした。
今回のチュートリアルの続編として、チュートリアル2を用意しました。