---
breaks: false
---
# OCaml で Discord 音楽ボットを書く
この記事は [Meta Language Advent Calendar 2023](https://adventar.org/calendars/8849) の 1 日目です。
**大切な告知:ぜひこの Advent Calendar に登録してください!!!!!**
OCaml で Discord ボットを書くためのライブラリ [Discord.ml](https://github.com/ushitora-anqou/discordml) を最近書きました。
これを使って、YouTube の URL を貼ると、その音声を再生してくれる音楽ボットを書きます。
とりあえずコードを見たい人は [Discord.ml の example code](https://github.com/ushitora-anqou/discordml/blob/master/example/main.ml) を参照してください。
## 前準備
Discord のトークンを用意します。[Discord Developer Portal](https://discord.com/developers/applications) から "New Application" で新規作成し、Bot タブから "Reset Token" してトークンを取得します。また同じページで "Message Content Intent" セクションのトグルを on にしておきます。
ここで取得したトークンは、以下では `DISCORD_TOKEN` 環境変数に入れて使用します。
作成した Discord bot を Discord サーバに招待します。招待リンクは、General application タブにある application id を使って
```
https://discord.com/api/oauth2/authorize?client_id={{ CLIENT_ID }}&permissions=3147776&scope=bot
```
のようにして作ります。
また [youtube-dl](https://github.com/ytdl-org/youtube-dl) をインストールしておきます。最近は後継の [yt-dlp](https://github.com/yt-dlp/yt-dlp) が良いらしいですが、どちらでも動きます。これらは、以下では `YOUTUBEDL_PATH` 環境変数にパスを入れて使用します。何も指定しないと `/usr/bin/youtube-dl` を見に行きます。
さらに FFmpeg をインストールしておきます。パスは `FFMPEG_PATH` に入れます。デフォルトでは `/usr/bin/ffmpeg` を見ます。
## コード
Discord.ml の example コードをそのまま使います。以下のようなコードを書きます。
```ocaml=
let handle_event token env ~sw:_ agent state =
let open Discord.Event in
function
| Dispatch (MESSAGE_CREATE msg) -> (
let guild_id = Option.get msg.guild_id in
let parsed = String.split_on_char ' ' msg.content in
match parsed with
| [ "!ping" ] ->
Logs.info (fun m -> m "ping");
if
Discord.Rest.make_create_message_param
~embeds:[ Discord.Object.make_embed ~description:"pong" () ]
()
|> Discord.Rest.create_message env ~token msg.channel_id
|> Result.is_error
then Logs.err (fun m -> m "Failed to send pong");
state
| [ "!join" ] ->
(match
agent
|> Discord.Agent.get_voice_states ~guild_id ~user_id:msg.author.id
with
| None -> ()
| Some vstate -> (
match vstate.Discord.Event.channel_id with
| None -> ()
| Some channel_id ->
agent |> Discord.Agent.join_channel ~guild_id ~channel_id));
state
| [ "!leave" ] ->
agent |> Discord.Agent.leave_channel ~guild_id;
state
| [ "!play"; url ] ->
Logs.info (fun m -> m "Playing %s" url);
agent |> Discord.Agent.play_voice ~guild_id ~src:(`Ytdl url);
state
| _ -> state)
| _ -> state
let () =
Logs.set_reporter (Logs_fmt.reporter ());
Logs.set_level (Some Logs.Info);
let token =
match Sys.getenv_opt "DISCORD_TOKEN" with
| Some s -> s
| None -> failwith "DISCORD_TOKEN not set"
in
let intents =
Discord.Intent.encode
[ GUILDS; GUILD_VOICE_STATES; GUILD_MESSAGES; MESSAGE_CONTENT ]
in
let youtubedl_path = Sys.getenv_opt "YOUTUBEDL_PATH" in
let ffmpeg_path = Sys.getenv_opt "FFMPEG_PATH" in
Eio_main.run @@ fun env ->
Mirage_crypto_rng_eio.run (module Mirage_crypto_rng.Fortuna) env @@ fun () ->
Eio.Switch.run @@ fun sw ->
let _consumer : _ Discord.Consumer.t =
Discord.Consumer.start env ~sw ~token ~intents ?ffmpeg_path ?youtubedl_path
(fun () -> ())
(handle_event token)
in
()
```
依存するライブラリなどは `dune` ファイルを見てください。
なんとなくコードを解説すると、本体は `Discord.Consumer.start` に渡されている `handle_event` 関数で、Discord 側でなにかイベントが起こるたびにこの関数が呼ばれます。この際、引数に `agent` が渡されるので、これ経由で Discord にメッセージを送ったり音声を送って再生したりします。
## ビルド
普通の OCaml プロジェクトで、OPAM と Dune を使ってビルドします。Eio を使っているので OCaml 5.0.0 以上を使う必要があることにだけ注意してください。
```
git clone https://github.com/ushitora-anqou/discordml.git
cd discordml
opam switch create . 5.0.0 --no-install
opam install . --deps-only
dune build example/main.exe
```
## 動かす
必要な環境変数を指定して起動します。
```
DISCORD_TOKEN="..." FFMPEG_PATH="..." YOUTUBEDL_PATH="..." dune exec example/main.exe
```
適当なボイスチャンネルに入って、テキストチャンネルで `!join` と打つと、bot が参加してきます。その状態で `!play URL` と打つと、URL に指定した YouTube の音声を再生してくれます。終わったら `!leave` で抜けていきます。
## 宣伝
Discord.ml を使って、Discord 用のテキスト読み上げ bot を書いています。Yomer という名前で、[GitHub で公開](https://github.com/ushitora-anqou/yomer)しています。よかったら使ってみてください。