# Disnake dynamic components
Library for simplified creation of buttons for Discord bots created using disnake.
- [x] Button support
- [x] Modal support
- [x] Select menu support
## Fast start
```python
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.
```python
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 abstract class `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
```python
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"))
```



## Moder Profile this Modal
```python
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"))
```




## Select Menu
```python
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"))
```



### More [examples](https://github.com/NodusLorden/DisnakeDynComponents/tree/master/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.
Raw data
{
"_id": null,
"home_page": "https://github.com/NodusLorden/DisnakeDynComponents",
"name": "disnake-dyn-components",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.12",
"maintainer_email": null,
"keywords": "disnake discord bot buttons modals selectmenu",
"author": "Lord_Nodus",
"author_email": "LordNodus@mail.ru",
"download_url": "https://files.pythonhosted.org/packages/3f/35/84def482327a8618e5caacc1354ebe57e2842cb97b233e0a4c814dfa7257/disnake_dyn_components-0.2.0.tar.gz",
"platform": null,
"description": "# Disnake dynamic components\r\n\r\nLibrary for simplified creation of buttons for Discord bots created using disnake.\r\n\r\n- [x] Button support\r\n- [x] Modal support\r\n- [x] Select menu support\r\n\r\n\r\n## Fast start\r\n\r\n```python\r\nimport disnake\r\nfrom disnake.ext import commands\r\nfrom disnake_dyn_components import DynComponents\r\nimport dotenv\r\nimport os\r\n\r\ndotenv.load_dotenv()\r\n\r\nbot = commands.Bot(intents=disnake.Intents.default())\r\n\r\ncomponents = DynComponents(bot)\r\n\r\n\r\n@components.create_button(\"say_hello\", label=\"Hello\")\r\nasync def hello_button(inter: disnake.MessageInteraction):\r\n await inter.send(\"Hello\")\r\n\r\n\r\n@bot.slash_command()\r\nasync def say_hello_buttons(inter: disnake.AppCmdInter):\r\n await inter.send(\r\n \"Click for say hello\",\r\n components=[hello_button()]\r\n )\r\n\r\n\r\nbot.run(os.getenv(\"TOKEN\"))\r\n```\r\n\r\n## Work protocol\r\n\r\nThe 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.\r\n> Important! The maximum length of custom_id is 100 characters, if this size is exceeded, you will receive an error\r\n\r\nSince `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.\r\n\r\nExample:\r\n\r\n> `ident=\"Message\"` and `ident=\"Message1\"` - have a collision\r\n> \r\n> `ident=\"Message1\"` and `ident=\"Message2\"` - do not have a collision\r\n\r\nIt 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.\r\n\r\nBasically, 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.\r\n\r\n```python\r\ndef button_data_collector(ident: str, button_data: list[str], sep=\"#\") -> str:\r\n if sep in ident:\r\n raise ValueError(\r\n f\"The ident `{ident}` has the symbol `{sep}` in it,\"\r\n f\" which cannot be used because it is a separator\"\r\n )\r\n for arg in button_data:\r\n if sep in arg:\r\n raise ValueError(\r\n f\"The argument `{arg}` has the symbol `{sep}` in it,\"\r\n f\" which cannot be used because it is a separator\"\r\n )\r\n return sep.join([ident] + button_data)\r\n\r\n\r\ndef button_data_separator(custom_id: str, sep=\"#\") -> list[str]:\r\n # The first argument needs to be removed because it is ident\r\n return custom_id.split(sep)[1:]\r\n\r\n\r\n@components.create_button(\r\n \"hello\",\r\n label=\"Send\",\r\n separator=button_data_separator,\r\n collector=button_data_collector\r\n)\r\nasync def message_button(inter: disnake.MessageInteraction, msg: str = \":)\"):\r\n await inter.send(msg)\r\n```\r\n\r\n### Data\r\n\r\nWhen 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 abstract class `Convertor`.\r\n\r\nAdditionally, support for types is implemented:\r\n- `int` convert to hex to save space\r\n- `bool` convert to int, this values `0` and `1`\r\nTypes without annotations will implicitly try to convert to `string` and when returned, they will remain as that type.\r\n\r\n# Examples\r\n## Button Pagination this shared file\r\n\r\n```python\r\nimport disnake\r\nfrom disnake.ext import commands\r\nimport os\r\nimport dotenv\r\nimport io\r\n\r\nfrom disnake_dyn_components import DynComponents\r\n\r\n\r\ndotenv.load_dotenv()\r\n\r\nbot = commands.InteractionBot(intents=disnake.Intents.default())\r\n\r\ncomponents = DynComponents(bot)\r\n\r\nfiles: list[io.BytesIO] = []\r\n\r\n\r\ndef get_button_and_text(file_index: int, page_index: int) -> tuple[disnake.ui.Button, disnake.ui.Button, str]:\r\n global files\r\n\r\n if len(files) <= file_index:\r\n prev_button = get_previous_button(file_index, page_index - 1)\r\n prev_button.disabled = True\r\n next_button = get_next_button(file_index, page_index + 1)\r\n next_button.disabled = True\r\n return prev_button, next_button, \"The file no longer exists\"\r\n\r\n file_buff = files[file_index]\r\n\r\n file_buff.seek(1000 * page_index)\r\n text = file_buff.read(1000).decode(\"utf-8\")\r\n\r\n file_buff.seek(1000 * page_index)\r\n\r\n return (\r\n get_previous_button(file_index, page_index - 1).update(disabled=page_index == 0),\r\n get_next_button(file_index, page_index + 1).update(disabled=not file_buff.read(1)),\r\n text\r\n )\r\n\r\n\r\n@components.create_button(\"next\", label=\">\")\r\nasync def get_next_button(inter: disnake.MessageInteraction, file_index: int, page_index: int):\r\n await inter.response.defer(with_message=False)\r\n prev_button, next_button, text = get_button_and_text(file_index, page_index)\r\n await inter.edit_original_message(\r\n f\"```\\n{text}\\n```\",\r\n components=[prev_button, next_button]\r\n )\r\n\r\n\r\n@components.create_button(\"previous\", label=\"<\")\r\nasync def get_previous_button(inter: disnake.MessageInteraction, file_index: int, page_index: int):\r\n await inter.response.defer(with_message=False)\r\n prev_button, next_button, text = get_button_and_text(file_index, page_index)\r\n await inter.edit_original_message(\r\n f\"```\\n{text}\\n```\",\r\n components=[prev_button, next_button]\r\n )\r\n\r\n\r\n@bot.slash_command()\r\nasync def send_file(\r\n inter: disnake.AppCmdInter,\r\n file: disnake.Attachment\r\n):\r\n global files\r\n await inter.response.defer(with_message=True)\r\n\r\n file_buff = io.BytesIO()\r\n await file.save(fp=file_buff, seek_begin=True)\r\n\r\n files.append(file_buff)\r\n file_index = len(files) - 1\r\n\r\n prev_button, next_button, text = get_button_and_text(file_index, 0)\r\n\r\n await inter.send(\r\n f\"```\\n{text}\\n```\",\r\n components=[prev_button, next_button]\r\n )\r\n\r\n\r\nbot.run(os.getenv(\"TOKEN\"))\r\n```\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n## Moder Profile this Modal\r\n\r\n```python\r\nimport disnake\r\nfrom disnake.ext import commands\r\nimport os\r\nimport dotenv\r\nimport datetime\r\n\r\nfrom disnake_dyn_components import DynComponents, DynTextInput\r\n\r\n\r\ndotenv.load_dotenv()\r\n\r\nbot = commands.Bot(intents=disnake.Intents.default())\r\n\r\n\r\n# Create a components store to search for collisions between them\r\ncomponents = DynComponents(bot)\r\n\r\n\r\n# Modals models\r\n@components.create_modal(\r\n \"mute_user\",\r\n \"Mute user\",\r\n {\r\n \"duration\": DynTextInput(\"Duration (minutes)\"),\r\n \"reason\": DynTextInput(\"Reason\", style=disnake.TextInputStyle.long)\r\n }\r\n)\r\nasync def mute_user_modal(inter: disnake.ModalInteraction, text_values, user_id: int):\r\n text_duration = text_values[\"duration\"]\r\n try:\r\n duration = float(text_duration)\r\n except ValueError:\r\n return await inter.send(\"Duration must be number\")\r\n\r\n member = inter.guild.get_member(user_id) or await inter.guild.fetch_member(user_id)\r\n await member.timeout(duration=duration * 60, reason=text_values[\"reason\"])\r\n\r\n await inter.send(\r\n f\"Member <@{user_id}> was muted\",\r\n allowed_mentions=disnake.AllowedMentions.none()\r\n )\r\n\r\n\r\n@components.create_modal(\r\n \"rename_user\",\r\n \"Rename\",\r\n {\r\n \"name\": DynTextInput(\"New Name\"),\r\n \"reason\": DynTextInput(\"Reason\", style=disnake.TextInputStyle.long)\r\n }\r\n)\r\nasync def rename_user_modal(inter: disnake.ModalInteraction, text_values, user_id: int):\r\n new_name = text_values[\"name\"]\r\n\r\n member = inter.guild.get_member(user_id) or await inter.guild.fetch_member(user_id)\r\n await member.edit(nick=new_name, reason=text_values[\"reason\"])\r\n\r\n await inter.send(\r\n f\"Member <@{user_id}> was renamed\",\r\n allowed_mentions=disnake.AllowedMentions.none()\r\n )\r\n\r\n\r\n# Buttons models\r\n@components.create_button(\"mute_user\", label=\"Mute\", style=disnake.ButtonStyle.primary)\r\nasync def mute_user_button(inter: disnake.MessageInteraction, user_id: int):\r\n if inter.message.interaction_metadata.user.id != inter.author.id:\r\n return await inter.response.send_message(\"Unavailable\")\r\n await inter.response.send_modal(mute_user_modal(user_id))\r\n\r\n\r\n@components.create_button(\"rename_user\", label=\"Rename\", style=disnake.ButtonStyle.green)\r\nasync def rename_user_button(inter: disnake.MessageInteraction, user_id: int):\r\n if inter.message.interaction_metadata.user.id != inter.author.id:\r\n return await inter.response.send_message(\"Unavailable\")\r\n await inter.response.send_modal(rename_user_modal(user_id))\r\n\r\n\r\n@bot.slash_command()\r\n@commands.has_permissions(moderate_members=True)\r\nasync def mod_profile(inter: disnake.AppCmdInter, member: disnake.Member):\r\n embed = (disnake.Embed(title=\"Example Member profile\", timestamp=datetime.datetime.now(datetime.UTC))\r\n .set_thumbnail(member.display_avatar.url)\r\n .set_author(name=bot.user.display_name, icon_url=bot.user.display_avatar.url)\r\n .set_footer(text=f\"Status: {member.status}\\nActivity: {member.activity}\\n\"))\r\n\r\n await inter.send(\r\n embed=embed,\r\n components=[\r\n # We create buttons by passing the parameters specified in the model\r\n rename_user_button(member.id),\r\n mute_user_button(member.id)\r\n ]\r\n )\r\n\r\n\r\nbot.run(os.getenv(\"TOKEN\"))\r\n```\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n## Select Menu\r\n\r\n```python\r\nimport disnake\r\nfrom disnake.ext import commands\r\nimport logging\r\nimport os\r\nimport dotenv\r\nimport datetime\r\nfrom disnake.ext.commands import Param\r\n\r\nfrom disnake_dyn_components import DynComponents, DynTextInput, DynMenu\r\n\r\n\r\ndotenv.load_dotenv()\r\n\r\nbot = commands.Bot(intents=disnake.Intents.default())\r\n\r\n\r\n# Create a components store to search for collisions between them\r\ncomponents = DynComponents(bot)\r\n\r\n\r\n@components.create_select_menu(\r\n \"send_message\",\r\n DynMenu.user_select(placeholder=\"Choose User for send message\"),\r\n separator=lambda x: x.split(\":\", 1)[1:] # for ignore : in user messages\r\n)\r\nasync def select_member_menu(inter: disnake.MessageInteraction, values, msg: str):\r\n await inter.response.defer(with_message=False)\r\n await inter.send(f\"Message for {values[0].mention}: {msg}\")\r\n\r\n\r\n@bot.slash_command()\r\n@commands.guild_only()\r\nasync def select_member(inter: disnake.AppCmdInter, msg: str = Param(max_length=50)):\r\n await inter.response.send_message(\r\n \"Select member\",\r\n components=[\r\n select_member_menu(msg)\r\n ]\r\n )\r\n\r\n\r\nbot.run(os.getenv(\"TOKEN\"))\r\n```\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n### More [examples](https://github.com/NodusLorden/DisnakeDynComponents/tree/master/examples) here.\r\n\r\n## Security\r\n\r\nTransferring important but not confidential data via `custom_id` components is safe. \r\nDiscord, for its part, checks the validity of components, including checking for `custom_id` matches,\r\nwhich is why you can safely transfer role ids via buttons for subsequent issuance by the bot,\r\nsince when simulating pressing a non-existent button with a template `custom_id` with a replaced role,\r\nDiscord will block such a request and it will not reach the bot client.\r\n",
"bugtrack_url": null,
"license": null,
"summary": "Library for quick creation of ui components of discord with the ability to pass additional parameters",
"version": "0.2.0",
"project_urls": {
"GitHub": "https://github.com/NodusLorden/DisnakeDynComponents",
"Homepage": "https://github.com/NodusLorden/DisnakeDynComponents"
},
"split_keywords": [
"disnake",
"discord",
"bot",
"buttons",
"modals",
"selectmenu"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "3f3584def482327a8618e5caacc1354ebe57e2842cb97b233e0a4c814dfa7257",
"md5": "dee8bfb576ca143a25ab4a8a868a0ff6",
"sha256": "e15c0939da6561135e64099d29688ed46c371b84b13a8c8d4921956c08b264ea"
},
"downloads": -1,
"filename": "disnake_dyn_components-0.2.0.tar.gz",
"has_sig": false,
"md5_digest": "dee8bfb576ca143a25ab4a8a868a0ff6",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.12",
"size": 12401,
"upload_time": "2025-01-06T12:02:45",
"upload_time_iso_8601": "2025-01-06T12:02:45.793784Z",
"url": "https://files.pythonhosted.org/packages/3f/35/84def482327a8618e5caacc1354ebe57e2842cb97b233e0a4c814dfa7257/disnake_dyn_components-0.2.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-01-06 12:02:45",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "NodusLorden",
"github_project": "DisnakeDynComponents",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"requirements": [
{
"name": "disnake",
"specs": [
[
"~=",
"2.10.1"
]
]
},
{
"name": "python-dotenv",
"specs": [
[
"~=",
"1.0.1"
]
]
},
{
"name": "pillow",
"specs": [
[
"~=",
"10.4.0"
]
]
}
],
"lcname": "disnake-dyn-components"
}