Skip to main content

Disnake dynamic components

Library for simplified creation of buttons for Discord bots created using disnake.

  • Button support
  • Modal support
  • Select menu support

Fast start

pip install disnake-dyn-components

import disnake
from disnake.ext import commands
from disnake_dyn_components import DynComponents
import dotenv
import os

dotenv.load_dotenv()

bot = commands.Bot(intents=disnake.Intents.default())

components = DynComponents(bot)


@components.create_button("say_hello", label="Hello")
async def hello_button(inter: disnake.MessageInteraction):
    await inter.send("Hello")


@bot.slash_command()
async def say_hello_buttons(inter: disnake.AppCmdInter):
    await inter.send(
        "Click for say hello",
        components=[hello_button()]
    )


bot.run(os.getenv("TOKEN"))

Work protocol

The library uses ident to determine the type of button pressed. The ident is placed in the custom_id of the button along with any data you choose to pass in.

Important! The maximum length of custom_id is 100 characters, if this size is exceeded, you will receive an error

Since ident is used to determine whether a button is pressed, and it is found at the beginning, in order to avoid collisions, each ident should not be nested within another.

Example:

ident="Message" and ident="Message1" - have a collision

ident="Message1" and ident="Message2" - do not have a collision

It is recommended to create all buttons at the beginning, rather than at runtime, since the DynButtons class automatically searches for collisions and raises an error if they are present.

Basically, ident and data are placed in a string with a : separator. If you need to change the transfer protocol, you can do this by passing functions for collecting and separating.

def button_data_collector(ident: str, button_data: list[str], sep="#") -> str:
    if sep in ident:
        raise ValueError(
            f"The ident `{ident}` has the symbol `{sep}` in it,"
            f" which cannot be used because it is a separator"
        )
    for arg in button_data:
        if sep in arg:
            raise ValueError(
                f"The argument `{arg}` has the symbol `{sep}` in it,"
                f" which cannot be used because it is a separator"
            )
    return sep.join([ident] + button_data)


def button_data_separator(custom_id: str, sep="#") -> list[str]:
    # The first argument needs to be removed because it is ident
    return custom_id.split(sep)[1:]


@components.create_button(
    "hello",
    label="Send",
    separator=button_data_separator,
    collector=button_data_collector
)
async def message_button(inter: disnake.MessageInteraction, msg: str = ":)"):
    await inter.send(msg)

Data

When you specify a parameter annotation, it is used to convert data from a string. You can create your own class that will handle type conversion from value to string and back. To make things easier, there is an protocol Convertor.

Additionally, support for types is implemented:

  • int convert to hex to save space
  • bool convert to int, this values 0 and 1 Types without annotations will implicitly try to convert to string and when returned, they will remain as that type.

Examples

Button Pagination this shared file

import disnake
from disnake.ext import commands
import os
import dotenv
import io

from disnake_dyn_components import DynComponents


dotenv.load_dotenv()

bot = commands.InteractionBot(intents=disnake.Intents.default())

components = DynComponents(bot)

files: list[io.BytesIO] = []


def get_button_and_text(file_index: int, page_index: int) -> tuple[disnake.ui.Button, disnake.ui.Button, str]:
    global files

    if len(files) <= file_index:
        prev_button = get_previous_button(file_index, page_index - 1)
        prev_button.disabled = True
        next_button = get_next_button(file_index, page_index + 1)
        next_button.disabled = True
        return prev_button, next_button, "The file no longer exists"

    file_buff = files[file_index]

    file_buff.seek(1000 * page_index)
    text = file_buff.read(1000).decode("utf-8")

    file_buff.seek(1000 * page_index)

    return (
        get_previous_button(file_index, page_index - 1).update(disabled=page_index == 0),
        get_next_button(file_index, page_index + 1).update(disabled=not file_buff.read(1)),
        text
    )


@components.create_button("next", label=">")
async def get_next_button(inter: disnake.MessageInteraction, file_index: int, page_index: int):
    await inter.response.defer(with_message=False)
    prev_button, next_button, text = get_button_and_text(file_index, page_index)
    await inter.edit_original_message(
        f"```\n{text}\n```",
        components=[prev_button, next_button]
    )


@components.create_button("previous", label="<")
async def get_previous_button(inter: disnake.MessageInteraction, file_index: int, page_index: int):
    await inter.response.defer(with_message=False)
    prev_button, next_button, text = get_button_and_text(file_index, page_index)
    await inter.edit_original_message(
        f"```\n{text}\n```",
        components=[prev_button, next_button]
    )


@bot.slash_command()
async def send_file(
        inter: disnake.AppCmdInter,
        file: disnake.Attachment
):
    global files
    await inter.response.defer(with_message=True)

    file_buff = io.BytesIO()
    await file.save(fp=file_buff, seek_begin=True)

    files.append(file_buff)
    file_index = len(files) - 1

    prev_button, next_button, text = get_button_and_text(file_index, 0)

    await inter.send(
        f"```\n{text}\n```",
        components=[prev_button, next_button]
    )


bot.run(os.getenv("TOKEN"))

img.png

img.png

img_1.png

Moder Profile this Modal

import disnake
from disnake.ext import commands
import os
import dotenv
import datetime

from disnake_dyn_components import DynComponents, DynTextInput


dotenv.load_dotenv()

bot = commands.Bot(intents=disnake.Intents.default())


# Create a components store to search for collisions between them
components = DynComponents(bot)


# Modals models
@components.create_modal(
    "mute_user",
    "Mute user",
    {
        "duration": DynTextInput("Duration (minutes)"),
        "reason": DynTextInput("Reason", style=disnake.TextInputStyle.long)
    }
)
async def mute_user_modal(inter: disnake.ModalInteraction, text_values, user_id: int):
    text_duration = text_values["duration"]
    try:
        duration = float(text_duration)
    except ValueError:
        return await inter.send("Duration must be number")

    member = inter.guild.get_member(user_id) or await inter.guild.fetch_member(user_id)
    await member.timeout(duration=duration * 60, reason=text_values["reason"])

    await inter.send(
        f"Member <@{user_id}> was muted",
        allowed_mentions=disnake.AllowedMentions.none()
    )


@components.create_modal(
    "rename_user",
    "Rename",
    {
        "name": DynTextInput("New Name"),
        "reason": DynTextInput("Reason", style=disnake.TextInputStyle.long)
    }
)
async def rename_user_modal(inter: disnake.ModalInteraction, text_values, user_id: int):
    new_name = text_values["name"]

    member = inter.guild.get_member(user_id) or await inter.guild.fetch_member(user_id)
    await member.edit(nick=new_name, reason=text_values["reason"])

    await inter.send(
        f"Member <@{user_id}> was renamed",
        allowed_mentions=disnake.AllowedMentions.none()
    )


# Buttons models
@components.create_button("mute_user", label="Mute", style=disnake.ButtonStyle.primary)
async def mute_user_button(inter: disnake.MessageInteraction, user_id: int):
    if inter.message.interaction_metadata.user.id != inter.author.id:
        return await inter.response.send_message("Unavailable")
    await inter.response.send_modal(mute_user_modal(user_id))


@components.create_button("rename_user", label="Rename", style=disnake.ButtonStyle.green)
async def rename_user_button(inter: disnake.MessageInteraction, user_id: int):
    if inter.message.interaction_metadata.user.id != inter.author.id:
        return await inter.response.send_message("Unavailable")
    await inter.response.send_modal(rename_user_modal(user_id))


@bot.slash_command()
@commands.has_permissions(moderate_members=True)
async def mod_profile(inter: disnake.AppCmdInter, member: disnake.Member):
    embed = (disnake.Embed(title="Example Member profile", timestamp=datetime.datetime.now(datetime.UTC))
             .set_thumbnail(member.display_avatar.url)
             .set_author(name=bot.user.display_name, icon_url=bot.user.display_avatar.url)
             .set_footer(text=f"Status: {member.status}\nActivity: {member.activity}\n"))

    await inter.send(
        embed=embed,
        components=[
            # We create buttons by passing the parameters specified in the model
            rename_user_button(member.id),
            mute_user_button(member.id)
        ]
    )


bot.run(os.getenv("TOKEN"))

img.png

img.png

img.png

img.png

Select Menu

import disnake
from disnake.ext import commands
import logging
import os
import dotenv
import datetime
from disnake.ext.commands import Param

from disnake_dyn_components import DynComponents, DynTextInput, DynMenu


dotenv.load_dotenv()

bot = commands.Bot(intents=disnake.Intents.default())


# Create a components store to search for collisions between them
components = DynComponents(bot)


@components.create_select_menu(
    "send_message",
    DynMenu.user_select(placeholder="Choose User for send message"),
    separator=lambda x: x.split(":", 1)[1:]  # for ignore : in user messages
)
async def select_member_menu(inter: disnake.MessageInteraction, values, msg: str):
    await inter.response.defer(with_message=False)
    await inter.send(f"Message for {values[0].mention}: {msg}")


@bot.slash_command()
@commands.guild_only()
async def select_member(inter: disnake.AppCmdInter, msg: str = Param(max_length=50)):
    await inter.response.send_message(
        "Select member",
        components=[
            select_member_menu(msg)
        ]
    )


bot.run(os.getenv("TOKEN"))

img.png

img.png

img.png

More examples here.

Security

Transferring important but not confidential data via custom_id components is safe. Discord, for its part, checks the validity of components, including checking for custom_id matches, which is why you can safely transfer role ids via buttons for subsequent issuance by the bot, since when simulating pressing a non-existent button with a template custom_id with a replaced role, Discord will block such a request and it will not reach the bot client.

Metadata

Release files for disnake-dyn-components 0.2.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for disnake-dyn-components 0.2.4
File Size Uploaded
disnake_dyn_components-0.2.4.tar.gz 13.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for disnake-dyn-components 0.2.4
File Interpreter ABI Platform
disnake_dyn_components-0.2.4-py3-none-any.whl Python 3 none any Details

Total release size: 24.9 kB

Release files / disnake_dyn_components-0.2.4.tar.gz

Download URL disnake_dyn_components-0.2.4.tar.gz
Size 13.3 kB
Tags Source
SHA-256 checksum
How to use checksums
c986a341d18cc6e067a32a1bed31904caa496ad4e052bb499f872fe4ce1234ca
BLAKE2b-256 checksum
How to use checksums
e350015d79762cefa9b5d099e187e0402475072170347af2510036cabefc21f5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.4

Release files / disnake_dyn_components-0.2.4-py3-none-any.whl

Download URL disnake_dyn_components-0.2.4-py3-none-any.whl
Size 11.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d23f8ac1ca33e645296be857945b85a5d407b223ef782ec1634a82f9d8e262ad
BLAKE2b-256 checksum
How to use checksums
f703bf3a6df78dfd530e455175bb673c5af3e7b23899ab4dee6855674fdef613
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.4

Release history Release notifications | RSS feed

This release

0.2.4 This release

2 release files

0.2.3

2 release files

0.2.2

1 release file

0.2.1

1 release file

0.1.3

1 release file

0.1.2

1 release file

0.1.1

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page