👑

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 のインストール

Shell
nimble install dimscord

簡単な Bot の例

ユーザーが!pingと送信したとき Bot がPong!と返す簡単な Bot を作成する。

コード

src/main.nim
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

実行

Shell
nim c -d:ssl -r src/main.nim

イベントハンドリング

以下にイベント(メッセージ作成, リアクション追加, メンバー参加等)の一覧が記載されている。
これを使用することにより, 「メッセージが作成(投稿)された際に返信する」等の機能を実装することができる。

https://krisppurg.github.io/dimscord/dimscord/objects/typedefs.html#Events

https://github.com/krisppurg/dimscord/blob/c012fa6d8378a9373aa7346aafe84ef6aba2a902/dimscord/objects/typedefs.nim#L849-L948

イベントハンドリングの形式は以下のようになっている。
イベント名はスネークケースでもキャメルケースでもどちらでも良い。
また, 引数名は基本的に何でも良いが, 引数の型と順番には注意すること。

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!と返す処理を実装しよう。
ファイル全体のコードは以下のようになる。(冒頭で示したコードと同じ)

src/main.nim
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

実行する。

Shell
nim c -r src/main.nim

これで, Discord のチャンネルに!pingと送信すると, Bot がPong!と返す。

メッセージ

Message

Message型の定義は以下のようになっている。

https://krisppurg.github.io/dimscord/dimscord/objects.html#Message

https://github.com/krisppurg/dimscord/blob/c012fa6d8378a9373aa7346aafe84ef6aba2a902/dimscord/objects/typedefs.nim#L147-L176

下記コードで示したように扱える。

# メッセージが作成された際にそのメッセージの情報をコンソールに表示する
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に返信先を指定する。

https://krisppurg.github.io/dimscord/dimscord/objects.html#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 (埋め込み)

リッチなメッセージを作成できる。

https://discord.com/safety/using-webhooks-and-embeds#title-4

https://message.style/

Embed型の定義は以下のようになっている。

https://krisppurg.github.io/dimscord/dimscord/objects.html#Embed

https://github.com/krisppurg/dimscord/blob/c012fa6d8378a9373aa7346aafe84ef6aba2a902/dimscord/objects/typedefs.nim#L106-L146

# メッセージが作成された際に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 は以下のようにインストールできる。

Shell
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)
    )
  )

アクティビティステータス

https://krisppurg.github.io/dimscord/dimscord/gateway.html#updateStatus%2CShard%2Cseq[ActivityStatus]%2Cstring

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

https://discord.com/developers/docs/events/gateway#gateway-intents

https://krisppurg.github.io/dimscord/dimscord/gateway.html#startSession%2CDiscordClient%2Cset[GatewayIntent]%2Cint%2Cint%2Cint%2Cint%2Cint

https://krisppurg.github.io/dimscord/dimscord/constants.html#GatewayIntent

https://github.com/krisppurg/dimscord/blob/d176db0ee84529cc004d86368243cada09e9a492/dimscord/constants.nim#L63-L84

メッセージコンポネント

ボタンやセレクトメニューなどのインタラクティブな要素をメッセージに追加することができる。

以下のコードを参照。

https://github.com/krisppurg/dimscord/blob/master/examples/message_components.nim

VC

以下のコードを参照。

https://github.com/krisppurg/dimscord/blob/master/examples/voice.nim

躓いた箇所

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