Discordアプリで音楽を再生できるチャットボットを作る｜Chatbot#1

こんにちは。フンです。
最近、NLP（Natural Language Processing）に興味を持ってるので、勉強しながらチャットボットのシリーズを作りたいと思います。
今回ですね、Discordというチャットアプリで、人のコマンドを理解し、音楽を再生できる見たいなチャットボットを作りましょう！
![ボット名は「Utadorei」に付ける＾＾](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344895/f57c07f5-bed4-7aa5-e515-18eb8a758d76.png)

#とりあえずボットの概観に行きましょう！
## ① ボットのスキルを先に決める
友達とボットとどっちとチャットするか別けるため、`!`をprefixとしてチャットする

| コマンド | 説明                                                                                    |
|----------|-----------------------------------------------------------------------------------------|
| play     | 音楽を再生する 例：`!play https://www.youtube.com/watch?v=SX_ViT4Ra7k`                  |
| skip     | 次の歌を再生する 例： `!skip`                                                           |
| stop     | 音楽をストップする 例： `!stop`                                                         |
| playnow  | 音楽を最初に置いて、再生する 例：`!playnow https://www.youtube.com/watch?v=SX_ViT4Ra7k` |
| join     | チャットボットをVoiceチャネルを呼ぶ                                                     |
| np       | 現在再生されている歌の情報を表示する                                                    |
| list     | 音楽リストを表示する                                                                    |

## ② ボットのサーバー構造
![Utadore#1 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344895/d91d78c8-176b-0f66-73d4-18cfb96490cb.png)

- ボットのサーバー：NodeJs、Express Framework、ytdl-coreのpackageを使う。`ytdl-core`はYoutubeからビデオをダウンロードしてデータを変換してDiscordサーバにWebsocketの通りストリーミングします。
- DiscordJSのAPI：`https://discordjs.guide/`に参考してください。
- Step-by-stepで説明するので、心配しないでくださいねw。

# 具体的なステップに始めます
## ① Discordでボットを登録する

- `https://discordapp.com/developers/applications/`にアクセスして、NewApplicationを登録する
- 登録してから、左のNavigationで「Bot」をクリックして、Tokenの所を注意してくださいね。後使うよう。
<img width="1005" alt="discord2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344895/2a45de83-de1c-7f50-6171-76f1f650d6bb.png">

- 以下にみるとボットの権限を設定する所だ。全部VoicePermissionをチェックしてね。生成される権限の数（PERMISSION_ID）とCLIENT_IDをコーピしてね。
<img width="987" alt="discord3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344895/7f603472-965d-1d29-27e4-ae4b2eaac854.png">

- ボットをDiscordに接続するため、以下のリンクをアクセスしてください：
`https://discordapp.com/oauth2/authorize?client_id=CLIENT_ID&scope=bot&permissions=PERMISSION_ID`

- DiscordのSeverでボットを見えると①が成功だ！
<img width="242" alt="discord4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344895/3b851119-54ae-b021-b495-5442b71a313c.png">

## ② Nodejsのパッケージをインストールします
以下のパッケージをインストールしてください。

discord.js - `npm i discord.js`
FFmpeg - `npm i ffmpeg-binaries`
node-opus - `npm i node-opus`
ytdl-core - `npm i ytdl-core`

## ③ ソースを書きましょう！
- とりあえず、Nodeのサーバーを作ります。アプリのメインJsファイルに

```server.js
var express = require('express');
var app = express();
var bodyParser = require('body-parser');
require('./discord/index');

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));

app.get('/', function (req, res) {
  res.send('Welcome to my word! I am Utadorei :)');
});

app.listen(process.env.PORT || 3000, function() {
  console.log('Utadorei app listening on port 3000!');
});

```

- 見やすくするようにdiscordのAPIを呼ぶのは別のファイルに書くます


```
require('./discord/index');
```

- ボットのTokenとprefixを`config.js`に書くます。

```
{
  "prefix": "!",
  "token": "xxx",
}

```

- ボットがメッセンジを処理するソース

```discord/index.js
const Discord = require('discord.js');
const { prefix, token } = require('./config.json');
const bot = new Discord.Client();
const HandleOrder = require('./services/handle_order.js');

bot.once('ready', () => {
  console.log('Ready!');
});

// メッセンジを聞く前にログインしましょう
bot.login(token);

// メッセンジが届いたら、このメソードを呼びます。
bot.on('message', message => {
  if (message.author.bot) { return; }
  
  // メッセンジの最初の言葉が!ならボットが実行する
  if (message.content.startsWith(prefix)) {
    HandleOrder.call(message);
  }
});
```

```discord/services/handle_order.js
const { prefix } = require('../config.json');
const ytdl = require('ytdl-core');

module.exports = {
  call: call,
  play: play,
  isValidCommand: isValidCommand,
  handlePlay: handlePlay,
  handleSkip: handleSkip,
  handleStop: handleStop,
  handlePlayNow: handlePlayNow
}

let servers = {};

function call(message) {
  const args = message.content.slice(prefix.length).split(' ');
  const command = args.shift().toLowerCase();

  switch (command) {
    case 'play':
      handlePlay(message, args[0]);
      break;
    case 'skip':
      handleSkip(message);
      break;
    case 'stop':
      handleStop(message);
      break;
    case 'playnow':
      handlePlayNow(message, args[0]);
      break;
    case 'join':
      handleJoin(message);
      break;
    case 'np':
      handleNp(message);
      break;
    case 'list':
      handleList(message);
      break;
  }
}

function handleJoin(message) {
  if (!message.guild.voiceConnection) {
    message.member.voiceChannel.join()
      .then(connection => {
      })
      .catch(console.log);
  }
}

function play(connection, message) {
  let server = servers[message.guild.id];
  let unplay_queue = getFirstUnPlayedQueue(server.queue);
  server.dispatcher = connection.playStream(
    ytdl(unplay_queue.url)
  );
  console.log('Playing ' + unplay_queue.url);
  unplay_queue.status = 1;
  server.dispatcher.on('end', function() {
    let unplay_queue = getFirstUnPlayedQueue(server.queue);
    if(unplay_queue) { play(connection, message); }
    else { connection.disconnect(); }
  });
}

function isValidCommand(message, url) {
  if (!url) {
    message.reply('Please type a link.');
    return false;
  }

  let reg = /^(http(s)?:\/\/)?((w){3}.)?youtu(be|.be)?(\.com)?\/.+/gm;
  if (!url.match(reg)) {
    message.reply('Is that a valid youtube link?');
    return false;
  }

  if (!message.member.voiceChannel) {
    message.reply('Please go to the voice channel first.');
    return false;
  }

  return true;
}

function handlePlay(message, url) {
  if (!isValidCommand(message, url)) { return; }

  if (!servers[message.guild.id]) { servers[message.guild.id] = {queue: []}; }
  servers[message.guild.id].queue.push({
    url: url,
    status: 0
  });

  console.log(servers[message.guild.id].queue);

  if (!message.guild.voiceConnection) {
    message.member.voiceChannel.join()
      .then(connection => {
        play(connection, message);
      })
      .catch(console.log);
  }
}

function handleSkip(message) {
  if (!servers[message.guild.id]) { return; }

  let server = servers[message.guild.id];
  if (server.dispatcher) { server.dispatcher.end(); }
}

function handleStop(message) {
  if (message.guild.voiceConnection) { message.guild.voiceConnection.disconnect(); }
}

function handlePlayNow(message, url) {
  let server = servers[message.guild.id];
  if(!server) {
    handlePlay(message, url);
    return;
  }

  server.queue.splice(1, 0, {
    url: url,
    status: 0
  });
  server.dispatcher.end();
}

function handleNp(message) {
  let server = servers[message.guild.id];
  if(!server || !server.queue) {
    message.reply("There is not things.");
    return;
  }
  let current_queue = getCurrentQueue(server.queue);
  if(!server) { return; }
  ytdl.getBasicInfo(current_queue.url).then(info => {
    message.reply(getInfoMsg(info, current_queue.url));
  });
}

function getCurrentQueue(queues) {
  return queues.filter(function(queue) {
    return queue.status === 1;
  }).slice(-1)[0];
}

function getFirstUnPlayedQueue(queues) {
  return queues.filter(function(queue) {
    return queue.status === 0;
  })[0];
}

async function handleList(message) {
  let server = servers[message.guild.id];
  if(!server || !server.queue) {
    message.reply("There is not things.");
    return;
  }
  let msg = "";
  server.queue.forEach(function(queue) {
    ytdl.getBasicInfo(queue.url).then(info => {
      msg += getInfoMsg(info, queue.url) + '\n --------------------------';
      console.log(msg);
    });
  });

  await delay();
  message.reply(msg);
}

function getInfoMsg(info, url) {
  return `\n Title: ${info.player_response.videoDetails.title} \n Author: ${info.author.name} \n Link: ${url}`;
}

function delay() {
  return new Promise(resolve => setTimeout(resolve, 1000));
}
```

- 今はサーバーをオープンしましょう！

```
npm start
```

<img width="774" alt="Screen Shot 2019-08-05 at 0.41.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344895/fb88f605-564b-5ab4-9df0-b9c06ae7f08d.png">

# まとめ
簡単なボットを開発完了になりました。
コマンドでボットをテストしてください。
音楽を聴きながら友達とゲームしてくださいね！

今度はコマンドだけではなく、自然言語をボットが理解し、音楽を再生できる機能を皆さんに紹介させていただきます。

見てくれてありがとうございました！。
Gitリンク：https://github.com/tqhung04/Utadorei