Metadata-Version: 2.5
Name: ezytools
Version: 0.2.0
Summary: A Python utility library for Yapper bot developers
License-Expression: LicenseRef-Proprietary
License-File: LICENSE
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# Ezytools

Build Yapper bots faster with reusable Python utilities for repetitive bot code.
Keep your command handlers and application logic; use Ezytools for argument
parsing, validation, moderation, role checks, embeds, and other common tasks.

All implementation lives in `src/ezytools/core.py`. The package initializer only
re-exports it. Install with `python -m pip install .`, or copy `core.py` into your
bot folder as `ezytools.py` to use it as a standalone file.

## Faster workflows inside your handlers

You write the command, decide who can run it, handle missing data, and choose the
response. These helpers remove repetitive lookup and validation code.

```python
@bot.command("roleinfo", "Look up a server role")
def roleinfo(msg):
    if not msg.server_id:
        return msg.reply("Use this in a server.")
    role = ezytools.find_role(bot, msg.server_id, msg.args)
    if role is None:
        return msg.reply("Role not found. Use a name, ID, or mention.")
    msg.reply(f'{role["name"]}: {role["id"]}')
```

| Helper | Repetitive work removed |
|---|---|
| `resolve_member(bot, server_id, target)` | Parse a user mention/ID, fetch membership, distinguish missing members from API failures |
| `find_role(bot, server_id, query)` | Fetch roles and find an ID, role mention, or exact name |
| `find_channel(bot, server_id, query, channel_type=None)` | Fetch/filter channels and find an ID, channel mention, or exact name |
| `member_role_ids(member)` | Normalize role IDs/dictionaries and remove duplicates |
| `require_roles(bot, msg, role_ids, match_all=False, message=None)` | Check server context and roles, reply on denial, return your branching decision |
| `update_roles(bot, server_id, user_id, add=(), remove=())` | Fetch membership once, skip unnecessary changes, preserve unrelated roles |
| `render_message(template, **values)` | Replace literal placeholders while preserving unrelated braces |

For example, inside your own authorized command:

```python
if not ezytools.require_roles(bot, msg, [STAFF_ROLE_ID]):
    return
changes = ezytools.update_roles(bot, msg.server_id, target_id, add=[MEMBER_ROLE_ID], remove=[GUEST_ROLE_ID])
msg.reply(f'Added {len(changes["added"])} roles; removed {len(changes["removed"])}.')
```

Inside your own `member_join` or `member_leave` event handler:

```python
text = ezytools.render_message(template, user=f"<@{event.user_id}>", username=event.username)
bot.send(channel_id, text)
```

Lookups return SDK dictionaries or `None`. Role/channel names match exactly,
ignoring case; duplicate names raise `ValueError` so you can request an ID.
Member lookup accepts IDs or mentions, not usernames. Only a member lookup's
404 is treated as missing; permission/network errors propagate.

`require_roles` defaults to any listed role; use `match_all=True` to require all.
It does not automatically authorize owners. `update_roles` applies removals before
additions and returns the IDs actually changed. It is not transactional: earlier
changes remain if a later API call fails. Your handler must decide which callers,
targets, and roles are allowed. `render_message` does no recursive substitution
or expression evaluation; supplied mentions and Markdown stay active.

`get_arguments(msg)` now reads quoted `msg.args` before the SDK's space-split
`msg.argv`. For strict quote validation with usage feedback, use
`command_arguments(msg, ...)` instead.

## Yapper bot-building helpers

These helpers use the synchronous Yapper SDK's command, event, member, role,
message, and interaction methods. They were checked against the `yapper.py`
SDK in this workspace; the live developer website could not be retrieved during
implementation. No extra runtime dependencies are added.

| Helper | Bot code it handles |
|---|---|
| `command_guard(bot, ...)` | Server-only commands, server-owner or any-role checks, and scoped cooldowns |
| `command_arguments(msg, ...)` | Quoted arguments, usage replies, and argument count validation |
| `reply_long(msg, text)` | Splitting and sending long replies |
| `has_role(bot, server_id, user_id, role_id)` | Checking member role IDs |
| `toggle_role(bot, server_id, user_id, role_id)` | Adding a missing role or removing an existing one |
| `install_welcome(bot, channel_id, server_id=..., ...)` | Server-specific welcome messages and optional autoroles |
| `register_help(bot, ...)` | Generating help from registered commands |
| `ButtonPaginator(bot)` | Sending pages with working Previous/Next buttons |

```python
import ezytools
import yapper

bot = yapper.Bot()  # reads YAPPER_BOT_TOKEN
pages = ezytools.ButtonPaginator(bot, timeout=120)

@bot.command("announce", "Post an announcement")
@ezytools.command_guard(bot, owner_only=True, cooldown=10)
def announce(msg):
    args = ezytools.command_arguments(
        msg, minimum=1, usage='/announce "your announcement"'
    )
    if args is None:
        return
    ezytools.reply_long(msg, " ".join(args))

@bot.command("guide", "Browse the guide")
@ezytools.command_guard(bot, cooldown=5)
def guide(msg):
    pages.send(msg, ["Getting started...", "Available features...", "Need help?..."])

ezytools.install_welcome(
    bot, "WELCOME_CHANNEL_ID", server_id="YOUR_SERVER_ID", role_id="MEMBER_ROLE_ID",
    template="Welcome {mention} to {server_name}!",
)
ezytools.register_help(bot)
bot.run()
```

Replace the example IDs with your IDs. The bot needs the platform permissions for
each action. `owner_only` means the **server owner**. Place `command_guard` below
`@bot.command`; `roles=["STAFF_ROLE_ID", "MOD_ROLE_ID"]` allows any listed role.
Cooldown `scope` can be `"user"` (per server and user), `"server"`, or `"channel"`.
Checks run before cooldowns are consumed, and API errors propagate to the SDK.
Guards apply to ordinary functions taking a message, not Cog instance methods.

`command_arguments` returns `None` after replying to invalid input; a valid empty
argument list returns `[]`. Role checks accept member `role_ids` or `roles` lists
of IDs or role dictionaries. `toggle_role` returns `True` when added and `False`
when removed. Check command access before allowing users to change roles.

Welcome fields are `{mention}`, `{username}`, `{user_id}`, `{server_name}`, and
`{server_id}`. Register one welcome listener per server. Role assignment happens
before sending the message; failures reach the SDK error handler.

Create one paginator per bot. Navigation belongs to the command author. Sessions
expire after a fixed timeout and are limited to 100 by default; old buttons then
reply with an expiry notice. Sessions and cooldowns reset on restart. Buttons stay
visible after expiry; no background timer edits messages. Keep page text, including
the page footer, within the platform's message limit. API errors propagate.

Help reads the command list when invoked, so later commands appear automatically.
`hidden=["internal"]` hides names from help only; guards enforce access.
See [the runnable example](examples/yapper_bot.py) for environment-based setup.

## Developer utilities

Install the package from this checkout (Python 3.10+):

```bash
python -m pip install .
```

All existing bot helpers and the following utilities are available through
`import ezytools`. These additions require no third-party runtime dependencies.

| Function | Purpose |
|---|---|
| `chunk_list(items, size)` | Split an iterable into lists |
| `paginate(items, page=1, per_page=10)` | Select a page, starting at page 1 |
| `truncate(text, max_length=100, suffix="...")` | Shorten text within a character limit |
| `slugify(text)` | Make a lowercase ASCII slug |
| `split_message(text, max_length=2000)` | Split text without losing characters |
| `unique(items)` | Remove duplicate hashable items, preserving order |
| `get_nested(data, path, default=None)` | Read nested dictionaries and lists |
| `clamp(value, minimum, maximum)` | Keep a value within inclusive bounds |
| `env_bool(name, default=False)` | Read a boolean environment setting |
| `env_int(name, default=None)` | Read an integer environment setting |
| `load_json(path, default=None)` | Read JSON, using a default for missing files |
| `save_json(path, data)` | Atomically replace a JSON file, creating parents |
| `generate_token(nbytes=32)` | Generate a URL-safe secret token |
| `retry(attempts=3, delay=1.0, backoff=2.0, exceptions=(Exception,))` | Retry a synchronous function |

```python
import ezytools

settings = ezytools.load_json("data/settings.json", default={})
settings["debug"] = ezytools.env_bool("DEBUG", default=False)
ezytools.save_json("data/settings.json", settings)

name = ezytools.get_nested({"users": [{"name": "Ada"}]}, "users.0.name")
for part in ezytools.split_message("A long response...", max_length=1000):
    print(part)

@ezytools.retry(attempts=3, delay=0.2, exceptions=(ConnectionError,))
def fetch_status(client):
    return client.get_status()
```

Sizes and page numbers must be positive integers. Pagination materializes the
iterable, and pages past the end return an empty list. Text limits count Python
characters rather than encoded bytes; splitting does not preserve Markdown blocks.
Slug generation discards characters that cannot be converted to ASCII.
Nested paths may also be lists of literal keys, such as `["key.with.dots", 0]`.

Boolean settings accept `true/false`, `yes/no`, `on/off`, and `1/0`, ignoring case
and surrounding whitespace. Invalid environment values raise `ValueError`.
Malformed JSON and permission errors propagate; they do not return the default.
Atomic saving prevents partially written files, but concurrent read/modify/write
operations still need caller-side locking. Existing in-memory moderation warnings
are not automatically persisted by these helpers.

Token size specifies random bytes, not the resulting string length. Retries include
the initial call in `attempts`, wait only between failed attempts, and re-raise the
last exception. Retry only operations that are safe to repeat. Async functions are
not supported by this decorator.

Run the test suite from the project directory without installing:

```powershell
$env:PYTHONPATH = "src"
python -m unittest discover -s tests -v
```

Ezytools is a Python utility library designed to make bot development simpler by handling repetitive tasks in reusable functions. It currently includes moderation helpers built to work with the Yapper Python SDK.

Instead of writing the same command logic over and over, you can call an Ezytools function and let it handle argument parsing, basic validation, API calls, and user-facing responses.

## ✨ Features

- 🛡️ **Moderation commands** — kick, ban, unban, mute, and unmute users
- ⚠️ **Warning system** — warn users, view their warnings, and clear warnings
- 🧩 **Yapper SDK integration** — designed to work with your existing Yapper bot
- 💬 **Argument handling** — parses command arguments and supports quoted text where possible
- ⏱️ **Duration parsing** — supports minute, hour, and day formats for timeouts
- 🛠️ **Reusable functions** — keep your bot's main file cleaner and easier to maintain
- 🚨 **Error handling** — provides readable responses for common moderation errors

## 📦 Project Structure

Your project can be organised like this:

```text
your-project/
├── ezytools.py
├── yapper.py
└── ezybot.py
```

- `ezytools.py` contains the reusable utility functions
- `yapper.py` provides the Yapper Python SDK
- `ezybot.py` contains your bot's commands and main logic

## ⚙️ Requirements

- Python 3.10 or later recommended
- The Yapper Python SDK for Yapper-specific moderation helpers

Ezytools uses Python's standard library for its current functionality.

## 🚀 Installation

### 1. Download Ezytools

Clone this repository:

```bash
git clone YOUR_REPOSITORY_URL
```

Replace `YOUR_REPOSITORY_URL` with the actual URL of this repository.

### 2. Add it to your project

Place `ezytools.py` in the same directory as your bot file.

```text
your-project/
├── ezytools.py
├── yapper.py
└── ezybot.py
```

### 3. Import Ezytools

```python
import ezytools
```

## 🛡️ Moderation Utilities

Ezytools currently provides these moderation helpers:

| Function | Description |
|---|---|
| `ezytools.kick(bot, msg)` | Kicks a user from the server |
| `ezytools.ban(bot, msg)` | Bans a user |
| `ezytools.unban(bot, msg)` | Removes a user's ban |
| `ezytools.mute(bot, msg)` | Times out a user |
| `ezytools.unmute(bot, msg)` | Removes a user's timeout |
| `ezytools.warn(bot, msg)` | Records a warning for a user |
| `ezytools.warnings(bot, msg)` | Displays a user's recorded warnings |
| `ezytools.clearwarnings(bot, msg)` | Clears a user's recorded warnings |
| `ezytools.help_text()` | Returns a list of the moderation commands |

## 💻 Usage

Ezytools is designed to work with command handlers from the Yapper Python SDK.

### Example: Registering a Kick Command

```python
import ezytools

@bot.command("kick", "Kick a member")
def kick_command(msg):
    ezytools.kick(bot, msg)
```

### Example: Registering Moderation Commands

```python
import ezytools

@bot.command("kick", "Kick a member")
def kick_command(msg):
    ezytools.kick(bot, msg)

@bot.command("ban", "Ban a member")
def ban_command(msg):
    ezytools.ban(bot, msg)

@bot.command("unban", "Unban a user")
def unban_command(msg):
    ezytools.unban(bot, msg)

@bot.command("mute", "Timeout a member")
def mute_command(msg):
    ezytools.mute(bot, msg)

@bot.command("unmute", "Remove a member's timeout")
def unmute_command(msg):
    ezytools.unmute(bot, msg)

@bot.command("warn", "Warn a member")
def warn_command(msg):
    ezytools.warn(bot, msg)

@bot.command("warnings", "View a member's warnings")
def warnings_command(msg):
    ezytools.warnings(bot, msg)

@bot.command("clearwarnings", "Clear a member's warnings")
def clearwarnings_command(msg):
    ezytools.clearwarnings(bot, msg)
```

**Important:** This example assumes you have already created your Yapper `bot` instance. The bot must have the permissions required for each moderation action.

## ⏱️ Timeout Durations

The mute helper accepts durations in these formats:

| Format | Meaning |
|---|---|
| `10m` | 10 minutes |
| `2h` | 2 hours |
| `1d` | 1 day |
| `30` | 30 minutes |

The maximum supported duration is **28 days**.

Example command:

```text
/mute 123456789 30m Spamming
```

Replace the example user ID with the actual user's ID.

## ⚠️ Warning System

Warnings are currently stored **in memory**.

This means:
- Warnings can be viewed using the warnings helper
- Warnings can be cleared using the clearwarnings helper
- Warnings are separated by server and user
- **Warnings are lost when the bot restarts**

Persistent warning storage would require a database or file-based storage system.

## 🧰 Error Handling

Ezytools handles common errors returned by moderation requests, including:

- Missing permissions
- Users or bans that cannot be found
- Invalid requests
- Incorrect command arguments

The bot still needs the correct server permissions, and the Yapper SDK must be configured correctly.

## 🔒 Security

- Keep your bot token private
- Store sensitive configuration in environment variables
- Never commit tokens or passwords to GitHub
- Give your bot only the permissions it needs
- Test moderation commands in a safe test server before using them in a real community

## 🛠️ Troubleshooting

### Ezytools cannot be imported

Make sure `ezytools.py` is in the same directory as your bot file, or installed in a location Python can import from.

### Moderation commands fail

Check that:
- The bot is running and connected
- The bot has the required server permissions
- The user ID is correct
- The command is being used inside a server

### Timeout duration is rejected

Use a valid duration such as `10m`, `2h`, `1d`, or a number of minutes. The maximum is 28 days.

### Warnings disappear

Warnings are stored in memory, so restarting the bot clears them.

## 🤝 Contributing

Contributions, bug reports, and suggestions are welcome.

When contributing:
1. Keep functions reusable
2. Follow standard Python formatting conventions
3. Test changes before submitting them
4. Update the README when features or usage change

## 📄 License

Ezytools is distributed under the [Ezytools Proprietary License](LICENSE).
See that file for the terms governing use, modification, and redistribution.

## 📌 About

Ezytools is a developing Python utility library focused on making bot development cleaner, simpler, and more reusable.

**Current focus:** moderation helpers and Yapper SDK integration.

More utilities can be added as the library develops.


