NimでDiscord Botを作る【Dimscord】
はじめに
Nim で Discord Bot を作れるライブラリであるDimscordを使ってみたので備忘録として残しておく。
前提
- Nim の基礎的な文法を理解している
- Nim の環境構築が済んでいる
- nimble の使い方を理解している
- Discord Developer Portal の使い方を理解している
- Bot のトークンを取得する方法など
使用した環境
- Ubuntu 24.04.2 LTS x86_64 (WSL 2)
- Nim v2.2.0
- nimble v0.16.1
- Dimscord v1.6.0
Dimscord について
- レポジトリ
- ドキュメント
- 例
Dimscord のインストール
nimble install dimscord
簡単な Bot の例
ユーザーが!ping
と送信したとき Bot がPong!
と返す簡単な Bot を作成する。
コード
import std/asyncdispatch
import dimscord
let discord {.mainClient.} = newDiscordClient("ここにトークンを入れる")
proc messageCreate(s: Shard, m: Message) {.event(discord).} =
if m.author.bot:
# Botによるメッセージは無視
return
if m.content == "!ping":
discard await discord.api.sendMessage(m.channelId, "Pong!")
waitFor discord.startSession
実行
nim c -d:ssl -r src/main.nim
イベントハンドリング
以下にイベント(メッセージ作成, リアクション追加, メンバー参加等)の一覧が記載されている。
これを使用することにより, 「メッセージが作成(投稿)された際に返信する」等の機能を実装することができる。
イベントハンドリングの形式は以下のようになっている。
イベント名はスネークケースでもキャメルケースでもどちらでも良い。
また, 引数名は基本的に何でも良いが, 引数の型と順番には注意すること。
proc イベント名(引数) {.event(discord).} =
# ここに処理を書く
例えば, 以下のイベントmessage_create
(メッセージ作成)を使用したい場合
message_create*: proc (s: Shard; msg: Message) {.async.}
以下のように書く。
proc messageCreate(s: Shard, m: Message) {.event(discord).} =
# ここに処理を書く
また, 以下のイベントmessage_reaction_add
(メッセージリアクション追加)を使用したい場合
message_reaction_add*: proc (s: Shard; msg: Message; emoji: Emoji) {.async.}
以下のように書く。
proc messageReactionAdd(s: Shard, m: Message, emoji: Emoji) {.event(discord).} =
# ここに処理を書く
さて, message_create
イベントを使用して, ユーザーによりメッセージが作成された際に Bot がPong!
と返す処理を実装しよう。
ファイル全体のコードは以下のようになる。(冒頭で示したコードと同じ)
import std/asyncdispatch
import dimscord
let discord {.mainClient.} = newDiscordClient("ここにトークンを入れる")
proc messageCreate(s: Shard, m: Message) {.event(discord).} =
if m.author.bot:
# Botによるメッセージは無視
return
if m.content == "!ping":
discard await discord.api.sendMessage(m.channelId, "Pong!")
waitFor discord.startSession
実行する。
nim c -r src/main.nim
これで, Discord のチャンネルに!ping
と送信すると, Bot がPong!
と返す。
メッセージ
Message
型
Message
型の定義は以下のようになっている。
下記コードで示したように扱える。
# メッセージが作成された際にそのメッセージの情報をコンソールに表示する
proc messageCreate(s: Shard, m: Message) {.event(discord).} =
if m.author.bot:
# Botによるメッセージは無視
return
echo "メッセージID: " & $m.id
echo "チャンネルID: " & $m.channelId
echo "メッセージの作成者のID: " & $m.author.id
echo "メッセージ内容: " & m.content
メッセージ送信
sendMessage
プロシージャを使用する。
# メッセージが作成された際に`Pong!`を送信する
proc messageCreate(s: Shard, m: Message) {.event(discord).} =
if m.author.bot:
# Botによるメッセージは無視
return
if m.content == "!ping":
discard await discord.api.sendMessage(m.channelId, "Pong!")
単なるメッセージではなく返信にする場合は, messageReference
に返信先を指定する。
# メッセージが作成された際に`Pong!`を返信する
proc messageCreate(s: Shard, m: Message) {.event(discord).} =
if m.author.bot:
# Botによるメッセージは無視
return
if m.content == "!ping":
discard await discord.api.sendMessage(
m.channelId, "Pong!",
option new MessageReference(
messageId = option(m.id),
channelId = option(m.channelId),
guildId = option(m.guildId)
)
)
メッセージ編集
editMessage
プロシージャを使用する。
# メッセージが作成された際にそのメッセージを編集する
proc messageCreate(s: Shard, m: Message) {.event(discord).} =
if m.author.bot:
# Botによるメッセージは無視
return
discard await discord.api.editMessage(m.channelId, m.id, "編集されました!")
メッセージ削除
deleteMessage
プロシージャを使用する。
# メッセージが作成された際にそのメッセージを削除する
proc messageCreate(s: Shard, m: Message) {.event(discord).} =
if m.author.bot:
# Botによるメッセージは無視
return
await discord.api.deleteMessage(m.channelId, m.id)
添付ファイル
DiscordFile
オブジェクトを使用する。
添付したいファイルがfiles/test.txt
にある場合:
proc messageCreate(s: Shard, m: Message) {.event(discord).} =
if m.author.bot:
# Botによるメッセージは無視
return
discard await discord.api.sendMessage(m.channelId,
content = "添付ファイルを送信します",
files = @[
DiscordFile(name: "files/test.txt")
]
)
Embed (埋め込み)
リッチなメッセージを作成できる。
Embed
型の定義は以下のようになっている。
# メッセージが作成された際にEmbedを送信する
proc messageCreate(s: Shard, m: Message) {.event(discord).} =
if m.author.bot:
# Botによるメッセージは無視
return
discard await discord.api.sendMessage(m.channelId,
embeds = @[Embed(
title: option("タイトル"),
description: option("説明文"),
url: option("https://example.com"),
color: option(0x00FF00),
footer: option(EmbedFooter(text: "フッター", iconUrl: option("https://example.com/footer.png"))),
image: option(EmbedImage(url: "https://example.com/image.png")),
thumbnail: option(EmbedThumbnail(url: "https://example.com/thumbnail.png")),
fields: option(@[
EmbedField(
name: "フィールド名1",
value: "フィールドの値1",
inline: option(true)
),
EmbedField(
name: "フィールド名2",
value: "フィールドの値2",
inline: option(false)
)
]),
)]
)
Future
型
例えば, getMessages
プロシージャを使用して, 指定したチャンネル内のメッセージを取得すると, Future[seq[Message]]
型の変数が返ってくる。
Future
型は非同期処理関係の型であり, 今回は, 処理の終了をwaitFor
プロシージャを使用して待ち, read
プロシージャを使用して値を取得する。
proc messageCreate(s: Shard, m: Message) {.event(discord).} =
if m.author.bot:
# Botによるメッセージは無視
return
# チャンネル内のメッセージを取得する処理
let channelFuture = discord.api.getChannel(m.channelId)
discard waitFor channelFuture
let messagesFuture = channelFuture.read[0].get.getMessages()
# 処理が終了するまで待つ
discard waitFor messagesFuture
# 処理が終了したかどうか判定
if messagesFuture.finished:
# 処理が失敗したかどうか判定
if messagesFuture.failed:
echo "メッセージの取得に失敗しました: " & messagesFuture.error.msg
return
# メッセージを取得してコンソールに表示
let messages = messagesFuture.read()
for message in messages:
echo message.content
Webhook
Webhook については以下を参照されたい。
Webhook で送信
executeWebhook
関数を使用する。
以下のような状況の場合, コードは次のようになる。
項目 | 値 |
---|---|
Webhook の URL | https://discord.com/api/webhooks/1234/abcd |
送信するメッセージ | こんにちは |
送信者名 | Webhook from Dimscord |
送信者のアイコン(アバター)の URL | https://example.com/avatar.png |
discard discord.api.executeWebhook(
webhookId = "1234",
webhookToken = "abcd",
content = "こんにちは",
username = option("Webhook from Dimscord"),
avatarUrl = option("https://example.com/avatar.png")
)
スラッシュコマンド
スラッシュコマンドについては, 以下を参照されたい。
ここでは, Dimscord に加えて, コマンドハンドル用のライブラリdimscmdを import して使用する。
dimscmd は以下のようにインストールできる。
nimble install dimscmd
また、以下のコードを追加する。
var cmd = discord.newHandler()
proc onReady(s: Shard, r: Ready) {.event(discord).} =
await cmd.registerCommands()
proc interactionCreate(s: Shard, i: Interaction) {.event(discord).} =
discard await cmd.handleInteraction(s, i)
スラッシュコマンド (引数なし)
以下の条件を満たすping
コマンドを作成する。
-
Pong!
を返す - コマンドの説明文:
ピン!
cmd.addSlash("ping") do ():
## ピン!
await discord.api.interactionResponseMessage(i.id, i.token,
kind = irtChannelMessageWithSource,
response = InteractionCallbackDataMessage(
content: "Pong!"
)
)
スラッシュコマンド (引数あり)
コマンドの説明文: テキストを表示
以下の引数を取るecho
コマンドを作成する。
引数名 | オプショナル? (引数省略可?) | 型 | 初期値 | 説明 |
---|---|---|---|---|
text |
いいえ | string |
なし | 表示するテキスト |
cmd.addSlash("echo") do (text {.help: "表示するテキスト".}: string):
## テキストを表示する
await discord.api.interactionResponseMessage(i.id, i.token,
kind = irtChannelMessageWithSource,
response = InteractionCallbackDataMessage(
content: text
)
)
スラッシュコマンド (オプショナル引数あり)
コマンドの説明文: サイコロを振る
以下の引数を取るサイコロコマンドdice
コマンドを作成する。
引数名 | オプショナル? (引数省略可?) | 型 | 初期値 | 説明 |
---|---|---|---|---|
quantities |
はい | Option[int] |
1 | 振るサイコロの個数 (初期値: 1) |
faces |
はい | Option[int] |
6 | 振るサイコロ一つの面数 (初期値: 6) |
cmd.addSlash("dice") do (
quantities {.help: "振るサイコロの個数 (初期値: 1)".}: Option[int],
faces {.help: "振るサイコロ一つの面数 (初期値: 6)".}: Option[int]
):
## サイコロを振る
let
quantitiesLiteral = quantities.get(1)
facesLiteral = faces.get(6)
proc rollDice(quantities, faces: int): int =
result = 0
for i in 0 ..< quantities:
result += rand(faces) + 1
await discord.api.interactionResponseMessage(i.id, i.token,
kind = irtChannelMessageWithSource,
response = InteractionCallbackDataMessage(
content: $quantitiesLiteral & "d" & $facesLiteral & " -> " & $rollDice(quantitiesLiteral, facesLiteral)
)
)
アクティビティステータス
proc onReady(s: Shard, r: Ready) {.event(discord).} =
echo "Ready as: " & $r.user
await s.updateStatus(
activity = some ActivityStatus(
name: "例",
kind: atPlaying,
url: option("https://example.com"),
),
status = "idle"
)
ActivitityStatus
内のkind
(ActivityType
)
名前 |
---|
atPlaying |
atStreaming |
atListening |
atWatching |
atCustom |
atCompeting |
updateStatus
プロシージャ内のstatus
ステータス | 説明 | アイコン |
---|---|---|
online |
オンライン | 🟢 |
idle |
退席中 | 🌙 |
dnd |
取り込み中 | 🔴 |
invisible |
オフライン | - |
offline |
オフライン | - |
Gateway Intents
メッセージコンポネント
ボタンやセレクトメニューなどのインタラクティブな要素をメッセージに追加することができる。
以下のコードを参照。
VC
以下のコードを参照。
躓いた箇所
Error: Client not registered
以下のようになっている場合:
let discord = new DiscordClient("ここにトークンを入れる")
以下のように修正する(mainClient
プラグマを使用する):
let discord {.mainClient.} = new DiscordClient("ここにトークンを入れる")
Error: type mismatch: got 'proc (s: Shard, m: Message): Future[system.void]{.gcsafe.}' for 'proc (s: Shard; m: Message): owned(Future[void]) =
不適切なイベントハンドリングにより発生する。
例えば, 存在しないイベント名が指定されている, 引数の型や順番が間違っているなど。
イベントハンドリングを参照。
Discussion