Skip to main content

A lightweight Discord bot library built on the raw Discord API.

Project description

wilnake

wilnake — библиотека для Discord-ботов на Python: прямой Gateway и REST API, без discord.py, disnake, nextcord и аналогов.

Возможности

  • Gateway + REST (aiohttp).
  • Prefix-команды и slash-команды.
  • Cogs, глобальные проверки и хуки before_invoke / after_invoke.
  • Модальные окна: декораторы, сборка через ModalBuilder, переиспользуемые наборы полей (combine_text_inputs), корректное слияние с show_modal.
  • Компоненты: ActionRow, Button, Select Menu (String/User/Role/Channel/Mentionable), обработчики по custom_id, ComponentContext.
  • Embed, Color, Intents.
  • Вспомогательные методы модерации.

Установка

pip install wilnake

Требования: Python 3.12+, пакет aiohttp.

CLI (Command Line Interface)

Wilnake включает удобный CLI для быстрого создания проектов.

Команды

wilnake --help              # Показать справку
wilnake version             # Показать версию
wilnake init --name NAME --prefix PREFIX   # Создать новый проект

Пример использования

# Создание нового бота
wilnake init --name my_bot --prefix .

# Переход в папку проекта
cd my_bot

# Запуск бота (не забудьте добавить токен в .env или bot.py)
python bot.py

CLI автоматически создаст структуру проекта с готовым примером бота, включая slash-команду /ping и обработчик события on_ready.

Быстрый старт

from wilnake import Bot

bot = Bot(
    prefix="!",
    status="dnd",
    activity="слежу за сервером",
    case_insensitive=True,
)


@bot.event
async def on_ready():
    print(f"{bot.username} успешно запущен и работает")


@bot.command("p", "пинг", description="Показывает пинг")
async def ping(ctx):
    await ctx.send("Понг!")


@bot.slash_command(name="ping", description="Показывает задержку бота")
async def ping_slash(ctx):
    await ctx.respond(f"Понг! {ctx.latency} мс")


bot.run("TOKEN")

Bot

from wilnake import Bot

bot = Bot(
    prefix="!",
    status="online",
    activity="работаю",
    case_insensitive=True,
    strip_after_prefix=True,
)

Параметры конструктора:

Параметр Назначение
prefix Префикс текстовых команд
status Статус в Discord (online, idle, dnd, invisible)
activity Текст активности
case_insensitive Команды без учёта регистра
strip_after_prefix Убирать пробелы после префикса
intents Объект Intents или целое число

Полезные свойства:

  • user, username, avatar_url, mention, latency, latency_ms
  • commands, slash_commands, modals, component_listeners (button_listeners как alias), intents

Модальные окна и компоненты:

  • bot.modal(...) / bot.add_modal / get_modal / remove_modal
  • bot.button(...) / bot.select(...) / add_component_listener / get_component_listener / remove_component_listener

Прочее:

  • run(token), start(token), close(), wait_until_ready(), sync_commands()
  • fetch_user, fetch_member, get_command, has_command, remove_command
  • add_cog / await add_cog_async, get_cog, remove_cog / await remove_cog_async

Intents

Файл wilnake/intents.py:

from wilnake import Bot, Intents

bot = Bot(
    prefix="!",
    intents=Intents.default().add(Intents.GUILD_MEMBERS),
)

Если intents не указаны, используется Intents.default().

Методы: default(), all(), none(), from_names(...), add, remove, has.

События

@bot.event
async def on_ready():
    print(bot.username)


@bot.event
async def on_message(ctx):
    print(ctx.content)


@bot.event
async def on_command_error(ctx, error):
    await ctx.send_error(str(error))


@bot.event
async def on_slash_command_error(ctx, error):
    await ctx.send_error(str(error))


@bot.event
async def on_modal_error(ctx, error):
    await ctx.send_error(str(error))


@bot.event
async def on_component_error(ctx, error):
    await ctx.send_error(str(error))

Жизненный цикл:

  • on_command, on_command_completion, on_command_error
  • on_slash_command, on_slash_command_completion, on_slash_command_error
  • on_modal_submit, on_modal_completion, on_modal_error
  • on_component, on_component_completion, on_component_error
  • on_interaction — для любого interaction (slash, modal, кнопка и т.д.)

Prefix-команды

@bot.command(description="Пинг")
async def ping(ctx):
    await ctx.send("Понг!")


@bot.command("p", "пинг", description="Пинг")
async def ping2(ctx):
    await ctx.send("Понг!")

Параметры декоратора: name, description, aliases, usage, cooldown, checks, hidden, enabled.

Аргументы парсятся через shlex (кавычки поддерживаются): !say "привет мир".

Slash-команды

from wilnake import OptionType, SlashOption


@bot.slash_command(
    name="avatar",
    description="Показывает аватар",
    options=[
        SlashOption(
            name="user",
            description="Пользователь",
            type=OptionType.USER,
            required=True,
        )
    ],
)
async def avatar(ctx):
    user = ctx.option("user")
    await ctx.respond(f"ID: {user['id']}")

OptionType: SUB_COMMAND, SUB_COMMAND_GROUP, STRING, INTEGER, BOOLEAN, USER, CHANNEL, ROLE, MENTIONABLE, NUMBER, ATTACHMENT.

Контексты

MessageContext

Свойства: bot, message, guild_id, channel_id, message_id, author, author_id, author_name, author_mention, content, args, args_text, command, invoked_with, latency, jump_url, channel_mention.

Методы: send, reply, typing, react, pin, unpin, edit, delete, dm, send_success, send_error.
У send / reply есть аргументы components и files для компонентов/файлов.

SlashContext

Дополнительно: command_name, options, resolved, responded, deferred.

Методы: option(...), respond, send, defer, followup, edit_original_response, delete_original_response, send_success, send_error, show_modal.
В методы ответа можно передать components для кнопок в сообщении.

ModalContext

После отправки формы: custom_id, options (словарь custom_id поля → значение), value(...), те же методы ответа, что у slash (в т.ч. components в respond / followup).

ComponentContext (кнопки и select)

custom_id, component_type, message, message_id, values, resolved, listener (зарегистрированный обработчик).
selected_values() — получить значения select уже с подстановкой объектов (user/role/channel) из resolved. defer_update() — отложить ответ без «думает…» для последующего редактирования сообщения.
update_message(...) — обновить сообщение, к которому привязана кнопка (interaction response type 7).

Embed

from wilnake import Color, Embed

embed = (
    Embed(title="Отчёт", description="Статус задачи", color=Color.BLURPLE)
    .set_author(name="wilnake", icon_url="https://example.com/icon.png")
    .add_field(name="Стадия", value="deploy", inline=True)
    .add_field(name="Результат", value="success", inline=True)
    .set_thumbnail(url="https://example.com/avatar.png")
    .set_image(url="https://example.com/banner.png")
    .set_footer(text="Сгенерировано ботом")
    .set_timestamp()
)

Поддерживаются все основные поля Discord embed API: author, footer, image, thumbnail, fields, timestamp.

Модальные окна

Ограничения Discord: до 5 полей ввода, заголовок до 45 символов. Константы: MAX_MODAL_TEXT_INPUTS, MAX_MODAL_TITLE_LENGTH.

Регистрация обработчика + открытие

Обработчик вешается на custom_id. В команде удобно открывать окно без колбэка — библиотека не затирает уже зарегистрированный обработчик: в show_modal подставляются новый заголовок и набор полей, а callback и базовые metadata берутся из модалки, созданной декоратором.

from wilnake import Bot, Modal, TextInput, TextInputStyle

bot = Bot(prefix="!")


@bot.modal(
    custom_id="feedback_modal",
    title="Обратная связь",
    components=[
        TextInput(
            custom_id="feedback_text",
            label="Твой отзыв",
            style=TextInputStyle.PARAGRAPH,
            min_length=5,
            max_length=400,
        )
    ],
)
async def feedback_submit(ctx):
    text = ctx.value("feedback_text")
    await ctx.send_success(f"Спасибо за отзыв:\n{text}")


@bot.slash_command(name="feedback", description="Открыть форму отзыва")
async def feedback(ctx):
    await ctx.show_modal(
        Modal(
            custom_id="feedback_modal",
            title="Обратная связь",
            components=(
                TextInput(
                    custom_id="feedback_text",
                    label="Твой отзыв",
                    style=TextInputStyle.PARAGRAPH,
                ),
            ),
        )
    )

Если передать в show_modal модалку с callback, она полностью заменит запись в реестре (как и add_modal).

Модульная сборка полей

Переиспользуемые куски формы и ModalBuilder:

from wilnake import (
    ModalBuilder,
    TextInput,
    TextInputStyle,
    combine_text_inputs,
)


def contact_fields() -> tuple[TextInput, ...]:
    return (
        TextInput(custom_id="email", label="Email", style=TextInputStyle.SHORT),
        TextInput(custom_id="note", label="Комментарий", style=TextInputStyle.PARAGRAPH),
    )


extra = (TextInput(custom_id="topic", label="Тема", style=TextInputStyle.SHORT),)

modal = (
    ModalBuilder("ticket_modal", "Заявка")
    .extend_from(combine_text_inputs(contact_fields(), extra))
    .add_short("priority", label="Приоритет", required=False, max_length=20)
    .build()
)

# или вручную:
modal2 = Modal(
    custom_id="ticket_modal",
    title="Заявка",
    components=combine_text_inputs(contact_fields(), extra),
)

combine_text_inputs принимает отдельные TextInput и/или итерируемые наборы полей и проверяет лимит в 5 полей.

Cog

from wilnake import Cog, modal, TextInput


class FeedbackCog(Cog):
    @modal(
        custom_id="report_modal",
        title="Жалоба",
        components=[TextInput(custom_id="reason", label="Причина")],
    )
    async def report_submit(self, ctx):
        await ctx.respond(ctx.value("reason"), ephemeral=True)

DM (личные сообщения)

from wilnake import Bot

bot = Bot(prefix="!")


@bot.command(description="Отправить инструкцию в ЛС")
async def dm_me(ctx):
    await ctx.dm("Проверь личные сообщения")
    await ctx.send("Отправил в ЛС")


@bot.command(description="Через объект автора")
async def dm_me2(ctx):
    await ctx.author.send("Привет из wilnake")

Кнопки

from wilnake import ActionRow, Bot, Button, ButtonStyle

bot = Bot(prefix="!")


@bot.slash_command(name="demo", description="Сообщение с кнопкой")
async def demo(ctx):
    row = ActionRow(
        (
            Button(ButtonStyle.PRIMARY, label="Нажми", custom_id="demo_btn"),
        )
    )
    await ctx.respond("Выбери действие", components=[row])


@bot.button(custom_id="demo_btn")
async def on_demo_btn(ctx):
    await ctx.respond("Нажато", ephemeral=True)

Кнопка-ссылка: Button(ButtonStyle.LINK, label="Сайт", url="https://example.com") — без custom_id, колбэк не вызывается.

Select Menu

from wilnake import (
    ActionRow,
    Bot,
    ChannelSelectMenu,
    MentionableSelectMenu,
    RoleSelectMenu,
    SelectOption,
    StringSelectMenu,
    UserSelectMenu,
)

bot = Bot(prefix="!")


@bot.slash_command(name="menus", description="Показать select-меню")
async def menus(ctx):
    row1 = ActionRow(
        (
            StringSelectMenu(
                custom_id="menu:string",
                placeholder="Выбери язык",
                options=(
                    SelectOption(label="Python", value="py"),
                    SelectOption(label="Go", value="go"),
                ),
            ),
        )
    )
    row2 = ActionRow((UserSelectMenu(custom_id="menu:user", min_values=1, max_values=2),))
    row3 = ActionRow((RoleSelectMenu(custom_id="menu:role"),))
    row4 = ActionRow((ChannelSelectMenu(custom_id="menu:channel"),))
    row5 = ActionRow((MentionableSelectMenu(custom_id="menu:mentionable"),))
    await ctx.respond("Выбери значение", components=[row1, row2, row3, row4, row5], ephemeral=True)


@bot.select(custom_id="menu:string")
async def on_string_select(ctx):
    await ctx.respond(f"String values: {ctx.values}", ephemeral=True)


@bot.select(custom_id="menu:user")
async def on_user_select(ctx):
    selected = ctx.selected_values()
    names = [item.get("username", item.get("name", "unknown")) if isinstance(item, dict) else str(item) for item in selected]
    await ctx.respond("Выбрано: " + ", ".join(names), ephemeral=True)

Moderation API

await bot.kick(guild_id, user_id, reason="Причина")
await bot.ban(guild_id, user_id, reason="Причина", delete_message_seconds=86400)
await bot.unban(guild_id, user_id, reason="Причина")
await bot.mute(guild_id, user_id, minutes=10, reason="Причина")
await bot.unmute(guild_id, user_id, reason="Причина")
await bot.set_nickname(guild_id, user_id, "Новый ник", reason="Причина")
await bot.set_slowmode(channel_id, 10)
deleted = await bot.purge_messages(channel_id, limit=25)

unmute проверяет активный timeout; если его нет — MemberNotMuted.

from wilnake import MemberNotMuted


@bot.event
async def on_slash_command_error(ctx, error):
    if isinstance(error, MemberNotMuted):
        await ctx.send_error("У этого пользователя нет активного мута.")
        return
    await ctx.send_error(str(error))

Checks и hooks

@bot.check
async def only_guild(ctx):
    return ctx.guild_id is not None


@bot.before_invoke
async def before_any_command(ctx):
    print("before", ctx.command.name)


@bot.after_invoke
async def after_any_command(ctx):
    print("after", ctx.command.name)

Cogs

from wilnake import Bot, Cog, command, slash_command

bot = Bot(prefix="!")


class Utility(Cog):
    @Cog.listener()
    async def on_ready(self):
        print(f"{self.bot.username} готов")

    @command("p", "пинг", description="Пинг")
    async def ping(self, ctx):
        await ctx.send("Понг!")

    @slash_command(name="hello", description="Приветствие")
    async def hello(self, ctx):
        await ctx.respond("Привет из cog!")


bot.add_cog(Utility(bot))
bot.run("TOKEN")

Хуки: cog_load, cog_unload (для асинхронных версий используйте add_cog_async / remove_cog_async).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wilnake-0.2.0.tar.gz (33.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

wilnake-0.2.0-py3-none-any.whl (29.7 kB view details)

Uploaded Python 3

File details

Details for the file wilnake-0.2.0.tar.gz.

File metadata

  • Download URL: wilnake-0.2.0.tar.gz
  • Upload date:
  • Size: 33.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.10

File hashes

Hashes for wilnake-0.2.0.tar.gz
Algorithm Hash digest
SHA256 8519204b870b148369ff0f9cc4af5007a5916039a01062db4f36a2f0cd6622a5
MD5 892a7bf1ee81df048b974cfd19827cd4
BLAKE2b-256 1e2dffbb9260747e44462a5293508eb7d6e25ee27c4deda2e4c482c7b4fbf443

See more details on using hashes here.

File details

Details for the file wilnake-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: wilnake-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 29.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.10

File hashes

Hashes for wilnake-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 70bb42e12adba9113792fbe110ea8aa77ff888ebdd4c3fe2cf9f6728246e5b08
MD5 6c2a9f69207f1e4f25da20e2ba58d4d2
BLAKE2b-256 e675e9b9659bb4e809205b639815b2add16fc4d0cbd186f72d17c2506d58d33e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page