Metadata-Version: 2.4
Name: suneord
Version: 1.0.3
Summary: A lightweight Discord bot library built on the raw Discord API.
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: aiohttp<4,>=3.9

# suneord

`suneord` — библиотека для 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`, обработчики по `custom_id`, `ComponentContext` (в т.ч. `defer_update`, `update_message`).
- `Embed`, `Color`, `Intents`.
- Вспомогательные методы модерации.

## Установка

```bash
pip install suneord
```

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

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

```python
from suneord 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

```python
from suneord 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`, `button_listeners`, `intents`

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

- `bot.modal(...)` / `bot.add_modal` / `get_modal` / `remove_modal`
- `bot.button(...)` / `add_button_listener` / `get_button_listener` / `remove_button_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

Файл `suneord/intents.py`:

```python
from suneord import Bot, Intents

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

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

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

## События

```python
@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-команды

```python
@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-команды

```python
from suneord 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`, `send_success`, `send_error`.  
У `send` / `reply` / `edit` есть необязательный аргумент `components: list[ActionRow]` для кнопок.

### 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 (кнопки)

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

## Embed

```python
from suneord import Color, Embed

embed = Embed(
    title="Заголовок",
    description="Описание",
    color=Color.BLURPLE,
)
embed.set_author(name="Suneord")
embed.add_field(name="Поле", value="Значение", inline=False)
embed.set_thumbnail(url="https://example.com/avatar.png")
embed.set_image(url="https://example.com/banner.png")
embed.set_footer(text="Footer")
embed.set_timestamp()
```

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

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

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

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

```python
from suneord 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`:

```python
from suneord 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

```python
from suneord 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)
```

## Кнопки

```python
from suneord 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`, колбэк не вызывается.

## Moderation API

```python
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`.

```python
from suneord 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

```python
@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

```python
from suneord 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`).
