Metadata-Version: 2.4
Name: suneord
Version: 1.0.1
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, которая работает через сырой Discord API без `discord.py`, `disnake`, `nextcord` и других готовых Discord-обёрток.

## Возможности

- Прямой Discord Gateway + REST.
- Prefix-команды.
- Slash-команды.
- `cogs`.
- `Embed`, `Color`.
- `MessageContext`, `SlashContext`, `ModalContext`.
- Модальные окна.
- Отдельный `Intents` API.
- Moderation-хелперы.

## Установка

```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-статус бота.
- `activity` - текст активности.
- `case_insensitive` - команды без учёта регистра.
- `strip_after_prefix` - убирать лишние пробелы после префикса.
- `intents` - `Intents` или `int`.

Свойства:

- `bot.user`
- `bot.username`
- `bot.avatar_url`
- `bot.mention`
- `bot.latency`
- `bot.latency_ms`
- `bot.commands`
- `bot.slash_commands`
- `bot.modals`
- `bot.intents`

Методы:

- `bot.run(token)`
- `bot.start(token)`
- `bot.close()`
- `bot.wait_until_ready()`
- `bot.sync_commands()`
- `bot.fetch_user(user_id)`
- `bot.fetch_member(guild_id, user_id)`
- `bot.get_command(name)`
- `bot.has_command(name)`
- `bot.remove_command(name)`
- `bot.add_cog(cog)`
- `await bot.add_cog_async(cog)`
- `bot.get_cog(name)`
- `bot.remove_cog(name)`
- `await bot.remove_cog_async(name)`
- `bot.add_modal(modal)`
- `bot.get_modal(custom_id)`
- `bot.remove_modal(custom_id)`

## Intents

Теперь интенты вынесены в отдельный файл `suneord/intents.py`.

Использование:

```python
from suneord import Bot, Intents

bot = Bot(
    prefix="!",
    status="dnd",
    activity="слежу за сервером",
    case_insensitive=True,
    intents=Intents.default().add(Intents.GUILD_MEMBERS),
)
```

Если `intents` не передать, библиотека сама возьмёт `Intents.default()`. Поэтому такой код тоже корректен:

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

Полезные методы:

- `Intents.default()`
- `Intents.all()`
- `Intents.none()`
- `Intents.from_names(...)`
- `intents.add(...)`
- `intents.remove(...)`
- `intents.has(...)`

## События

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

Примеры:

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


@bot.event
async def on_command(ctx):
    print(ctx.command.name)


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

Дополнительные lifecycle-события:

- `on_command_completion`
- `on_slash_command_completion`
- `on_modal_submit`
- `on_modal_completion`

## Prefix-команды

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

С алиасами:

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

Или:

```python
@bot.command(aliases=("p", "пинг"))
async def ping(ctx):
    await ctx.send("Понг!")
```

Поддерживаемые параметры:

- `name`
- `description`
- `aliases`
- `usage`
- `cooldown`
- `checks`
- `hidden`
- `enabled`

Аргументы prefix-команд парсятся через `shlex`, поэтому строки в кавычках работают нормально:

```python
!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`:

- `STRING`
- `INTEGER`
- `BOOLEAN`
- `USER`
- `CHANNEL`
- `ROLE`
- `MENTIONABLE`
- `NUMBER`
- `ATTACHMENT`

## Context API

### MessageContext

Свойства:

- `ctx.bot`
- `ctx.message`
- `ctx.guild_id`
- `ctx.channel_id`
- `ctx.message_id`
- `ctx.author`
- `ctx.author_id`
- `ctx.author_name`
- `ctx.author_mention`
- `ctx.content`
- `ctx.args`
- `ctx.args_text`
- `ctx.command`
- `ctx.invoked_with`
- `ctx.latency`
- `ctx.jump_url`
- `ctx.channel_mention`

Методы:

- `ctx.send(...)`
- `ctx.reply(...)`
- `ctx.typing()`
- `ctx.react("🔥")`
- `ctx.pin()`
- `ctx.unpin()`
- `ctx.edit(...)`
- `ctx.delete()`
- `ctx.send_success(...)`
- `ctx.send_error(...)`

### SlashContext

Свойства:

- `ctx.bot`
- `ctx.interaction`
- `ctx.guild_id`
- `ctx.channel_id`
- `ctx.user`
- `ctx.author`
- `ctx.author_id`
- `ctx.author_name`
- `ctx.author_mention`
- `ctx.command`
- `ctx.command_name`
- `ctx.options`
- `ctx.latency`
- `ctx.channel_mention`
- `ctx.responded`
- `ctx.deferred`

Методы:

- `ctx.option("name")`
- `ctx.respond(...)`
- `ctx.send(...)`
- `ctx.defer(...)`
- `ctx.followup(...)`
- `ctx.edit_original_response(...)`
- `ctx.delete_original_response()`
- `ctx.send_success(...)`
- `ctx.send_error(...)`
- `ctx.show_modal(modal)`

### ModalContext

Свойства:

- `ctx.bot`
- `ctx.interaction`
- `ctx.custom_id`
- `ctx.options`
- `ctx.user`
- `ctx.author`
- `ctx.author_id`
- `ctx.responded`
- `ctx.deferred`

Методы:

- `ctx.value("field_id")`
- `ctx.respond(...)`
- `ctx.send(...)`
- `ctx.defer(...)`
- `ctx.followup(...)`
- `ctx.edit_original_response(...)`
- `ctx.delete_original_response()`
- `ctx.send_success(...)`
- `ctx.send_error(...)`

## 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 modals.

Пример:

```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):
    modal = Modal(
        custom_id="feedback_modal",
        title="Обратная связь",
        components=(
            TextInput(
                custom_id="feedback_text",
                label="Твой отзыв",
                style=TextInputStyle.PARAGRAPH,
            ),
        ),
    )
    await ctx.show_modal(modal)
```

Для `cogs` доступен отдельный декоратор:

```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)
```

## 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`:

- `bot.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 lifecycle доступны:

- `cog_load`
- `cog_unload`
