disnake-dyn-components


Namedisnake-dyn-components JSON
Version 0.1.3 PyPI version JSON
download
home_pagehttps://github.com/NodusLorden/DisnakeDynComponents
SummaryModule for quick creation of functional buttons for disnake
upload_time2024-12-08 09:42:16
maintainerNone
docs_urlNone
authorLord_Nodus
requires_python>=3.11
licenseNone
keywords discord disnake button bot
VCS
bugtrack_url
requirements disnake python-dotenv pillow
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Disnake dynamic components

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

- [x] Button support
- [ ] Modal support
- [ ] Select menu support


## Fast start

```py
import disnake
from disnake.ext import commands
from disnake_dyn_components import DynButtons
import dotenv
import os


dotenv.load_dotenv()

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

buttons = DynButtons(bot)


@buttons.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.

```py
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:]


@buttons.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.

## Example
### Pagination

```py
import disnake
from disnake.ext import commands
import logging
import os
import dotenv
import io

from disnake_dyn_components import DynButtons


logging.basicConfig(level=logging.WARN)
log = logging.getLogger(__name__)

dotenv.load_dotenv()

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


buttons = DynButtons(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")

    prev_button = get_previous_button(file_index, page_index - 1)
    # Disable button in first page
    if page_index == 0:
        prev_button.disabled = True

    next_button = get_next_button(file_index, page_index + 1)
    # Disable if this is the only page.
    if not file_buff.read(1):
        next_button.disabled = True
    file_buff.seek(1000 * page_index)

    return prev_button, next_button, text


@buttons.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]
    )


@buttons.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"))
```

More [examples](https://github.com/NodusLorden/DisnakeDynComponents/tree/master/examples) here.

## Security

You can safely transmit some important, but not confidential data,
since the `custom_id` of the components is transmitted to the clients of users.
Discord on its side checks the validity of the components, including checking the matches of `custom_id`,
because of which you can safely transmit the role id through the 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.11",
    "maintainer_email": null,
    "keywords": "discord disnake button bot",
    "author": "Lord_Nodus",
    "author_email": "LordNodus@mail.ru",
    "download_url": "https://files.pythonhosted.org/packages/71/2d/232ee420b095d3e1b9b6b36dc9f837daeab1455e891ab4ea8b75a1e4f7c6/disnake_dyn_components-0.1.3.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- [ ] Modal support\r\n- [ ] Select menu support\r\n\r\n\r\n## Fast start\r\n\r\n```py\r\nimport disnake\r\nfrom disnake.ext import commands\r\nfrom disnake_dyn_components import DynButtons\r\nimport dotenv\r\nimport os\r\n\r\n\r\ndotenv.load_dotenv()\r\n\r\nbot = commands.Bot(intents=disnake.Intents.default())\r\n\r\nbuttons = DynButtons(bot)\r\n\r\n\r\n@buttons.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\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```py\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@buttons.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## Example\r\n### Pagination\r\n\r\n```py\r\nimport disnake\r\nfrom disnake.ext import commands\r\nimport logging\r\nimport os\r\nimport dotenv\r\nimport io\r\n\r\nfrom disnake_dyn_components import DynButtons\r\n\r\n\r\nlogging.basicConfig(level=logging.WARN)\r\nlog = logging.getLogger(__name__)\r\n\r\ndotenv.load_dotenv()\r\n\r\nbot = commands.Bot(intents=disnake.Intents.default())\r\n\r\n\r\nbuttons = DynButtons(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    prev_button = get_previous_button(file_index, page_index - 1)\r\n    # Disable button in first page\r\n    if page_index == 0:\r\n        prev_button.disabled = True\r\n\r\n    next_button = get_next_button(file_index, page_index + 1)\r\n    # Disable if this is the only page.\r\n    if not file_buff.read(1):\r\n        next_button.disabled = True\r\n    file_buff.seek(1000 * page_index)\r\n\r\n    return prev_button, next_button, text\r\n\r\n\r\n@buttons.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@buttons.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\nMore [examples](https://github.com/NodusLorden/DisnakeDynComponents/tree/master/examples) here.\r\n\r\n## Security\r\n\r\nYou can safely transmit some important, but not confidential data,\r\nsince the `custom_id` of the components is transmitted to the clients of users.\r\nDiscord on its side checks the validity of the components, including checking the matches of `custom_id`,\r\nbecause of which you can safely transmit the role id through the 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": "Module for quick creation of functional buttons for disnake",
    "version": "0.1.3",
    "project_urls": {
        "Homepage": "https://github.com/NodusLorden/DisnakeDynComponents"
    },
    "split_keywords": [
        "discord",
        "disnake",
        "button",
        "bot"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "712d232ee420b095d3e1b9b6b36dc9f837daeab1455e891ab4ea8b75a1e4f7c6",
                "md5": "6909fb25835970f468dc6d91a3a463fe",
                "sha256": "46bfd8a9627228beca6a8408438123bd0608ca0250f4c76afd15da8a1ae60200"
            },
            "downloads": -1,
            "filename": "disnake_dyn_components-0.1.3.tar.gz",
            "has_sig": false,
            "md5_digest": "6909fb25835970f468dc6d91a3a463fe",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.11",
            "size": 6399,
            "upload_time": "2024-12-08T09:42:16",
            "upload_time_iso_8601": "2024-12-08T09:42:16.294838Z",
            "url": "https://files.pythonhosted.org/packages/71/2d/232ee420b095d3e1b9b6b36dc9f837daeab1455e891ab4ea8b75a1e4f7c6/disnake_dyn_components-0.1.3.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-12-08 09:42:16",
    "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.9.2"
                ]
            ]
        },
        {
            "name": "python-dotenv",
            "specs": [
                [
                    "~=",
                    "1.0.1"
                ]
            ]
        },
        {
            "name": "pillow",
            "specs": [
                [
                    "~=",
                    "10.4.0"
                ]
            ]
        }
    ],
    "lcname": "disnake-dyn-components"
}
        
Elapsed time: 0.39671s