--- 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)しています。よかったら使ってみてください。