TgrEzLi 1.0.1
Telegram bot library with sync-style API, handlers for messages/commands/callbacks, and optional embedded API server.
Requires Python: >=3.12

# TgrEzLi

Telegram bot library with a **sync-style API**, handlers for messages, commands, and callbacks, and an optional **embedded HTTP server** for custom POST endpoints. Built on [python-telegram-bot](https://pypi.org/project/python-telegram-bot/) and [ezlog](https://pypi.org/project/ezlog-py/) for logging.

- **Sync-style usage**: register handlers with decorators, use `TgMsg` / `TgCmd` / `TgCb` / `TgArgs` in handlers (thread-local context).
- **Multiple chats**: connect with a `{chat_name: chat_id}` dict; restrict handlers per chat.
- **Optional API server**: expose custom POST routes; call them with the `TReq` client.
- **Credential encryption**: store token and chats in an encrypted file with **PyCypherLib** (included by default).

---

## Install

```bash
pip install TgrEzLi
```

Dependencies: `ezlog-py`, `python-telegram-bot`, **PyCypherLib**, `requests`.

---

## Quick start

**After `connect()`** the bot starts polling in a **background thread**, so **`send_msg()` and handlers work immediately** (no need to call `run()`). Use **`run()`** only to **block the main thread** until you press Ctrl+C or call `stop()` (e.g. in a script so the process does not exit).

```python
from TgrEzLi import TEL, TgMsg, TgCmd, TgCb, TgArgs, TReq, log

bot = TEL()

# 1) Connect: starts polling in background. send_msg() works right away.
TOKEN = "123456789:ABCDEFGHIJKLMNO"
CHAT_DICT = {"chat1": "123456789", "chat2": "789456123"}
bot.connect(TOKEN, CHAT_DICT)

# You can send without calling run() (e.g. from REPL)
bot.send_msg("Ciao a tutti")

# 2) Register handlers (before or after connect)
@bot.on_message()
def on_message():
    bot.send_msg(f"Hi {TgMsg.userName}! You wrote: {TgMsg.text}")

@bot.on_command("start")
def on_start():
    bot.send_msg(f"Welcome {TgCmd.userName}!")
    if TgCmd.args:
        bot.send_msg(f"Args: {TgCmd.args}")

# 3) Optional: block main thread until Ctrl+C (so the script does not exit)
bot.run()
```

From the **REPL**: after `connect()` you can call `bot.send_msg("...")` and it is sent; handlers are active. If you **redefine** a handler (same function name), the new one **replaces** the previous one (no duplicate execution).

---

## Configuration

You can pass a **dict config** (`TgrEzLiConfig`) to `TEL()`; all keys are optional. If **`connection`** contains `token` and `chats`, **`connect()` is called automatically** in `__init__` (you can still call `connect()` yourself later for REPL or password-based login).

```python
from TgrEzLi import TEL, log

# Optional: connect on init
bot = TEL({
    "connection": {
        "token": "123456789:ABCDEF...",
        "chats": {"chat1": "123456789", "chat2": "789456123"},
    },
    "logging": {
        "debug": False,           # default: extra debug logs
        "save_log_file": False,   # default: do not append to file
        "log_file_path": "TgrEzLi.log",
    },
    "api_server": {
        "host": "localhost",
        "port": 9999,
    },
})
# If connection was provided, bot is already connected; send_msg() works.
```

**Logging:** `save_log_file` (default `False`) controls writing log lines to a file; `log_file_path` is the path. The bot’s console output always uses ezlog. You can also use the preconfigured **`log`** instance:

```python
from TgrEzLi import log
log.info("Hello")
log.success("Done")
```

**Mutate after creation:**

- **`bot.set_save_log(flag)`** – enable/disable writing log lines to the file (`save_log_file`).
- **`bot.set_host(host)`** / **`bot.set_port(port)`** – API server host/port.

**Legacy:** You can still pass a **`TgrezliConfig`** (dataclass) for backward compatibility; it is converted to the new structure internally.

---

## Connecting and running

- **`connect(token, chat_dict)`** – Prepares the bot and **starts polling in a background thread**. `send_msg()` and handlers work immediately (e.g. from REPL you can send without calling `run()`).
- **`run()`** – **Optional.** Blocks the main thread until `stop()` (e.g. Ctrl+C). Use in scripts so the process does not exit. If API routes are registered, starts the API server when you call `run()`.
- **`stop()`** – Stops polling and API server; if `run()` was waiting, it returns. Safe to call from a signal handler (Ctrl+C) or from another thread.

**Direct:**

```python
bot.connect(TOKEN, {"chat1": "123456789", "chat2": "789456123"})
bot.send_msg("Hello!")  # works immediately
# ... register handlers ...
bot.run()  # optional: block until Ctrl+C
```

**Encrypted storage (PyCypherLib):**

```python
# First time: save token and chats with a password (then connects)
bot.signup(TOKEN, CHAT_DICT, "your_password")
bot.run()

# Later: load and connect
bot.login("your_password")
bot.send_msg("Hi from login")
bot.run()
```

Data is stored in `tgrdata.cy` by default (PyCypherLib).

---

## Sending content

All send methods accept an optional **`chat`**: `None` (default/first chat), a **string** (one chat name), or a **list/set** of names.

| Method | Description |
|--------|--------------|
| **`send_msg(text, chat=None)`** | Text message |
| **`reply_to_msg(text, msg_id, chat=None)`** | Reply to a message |
| **`send_img(photo_path, caption=None, chat=None)`** | Photo |
| **`send_file(file_path, caption=None, chat=None)`** | Document |
| **`send_position(latitude, longitude, chat=None)`** | Location |
| **`send_buttons(text, buttons, chat=None)`** | Message with inline keyboard (see below) |
| **`send_log(limit=None, chat=None)`** | Last `limit` lines of log file (or full) |
| **`send_info(chat=None)`** | Bot info (chats, handlers, API server) |
| **`send_registered_handlers(chat=None)`** | List of registered handlers (debug) |

**Inline buttons:** `buttons` is a list of rows; each row is a list of `{"text": "...", "value": "..."}` (value = callback_data).

```python
bot.send_buttons("Choose:", [
    [{"text": "Red", "value": "red"}, {"text": "Blue", "value": "blue"}],
    [{"text": "Cancel", "value": "cancel"}],
])
```

CamelCase aliases exist: `sendMsg`, `replyToMsg`, `sendImg`, `sendFile`, `sendPosition`, `sendButtons`, `sendLog`, `sendInfo`, `sendRegisteredHandlers`.

---

## Handlers

**Replace-by-name:** If you register a handler whose function has the same **name** as an existing one (same type: message, command, or callback), the new handler **replaces** the old one. So in the REPL you can redefine `handle_message` and only the latest version will run.

Handlers run in a **background thread**; inside them use the global proxies **`TgMsg`**, **`TgCmd`**, **`TgCb`**, **`TgArgs`** to access the current payload. **`chat`** filters which chats trigger the handler: `None` = default chat, string = one chat, list/set = multiple chats.

### Message handler

```python
@bot.on_message()           # default chat
@bot.on_message("chat1")    # only chat1
@bot.on_message(["chat1", "chat2"])

def on_message():
    # TgMsg: .text, .msgId, .chatId, .userId, .userName, .timestamp, .raw_update
    bot.send_msg(f"Echo: {TgMsg.text}")
```

### Command handler

```python
@bot.on_command("start")
@bot.on_command("help", "chat1")

def on_start():
    # TgCmd: .command, .args, .msgId, .chatId, .userId, .userName, .timestamp, .raw_update
    bot.send_msg(f"Welcome {TgCmd.userName}!")
    if TgCmd.args:
        bot.send_msg(f"You said: {TgCmd.args}")
```

### Callback handler (inline buttons)

```python
@bot.on_callback("chat1")

def on_callback():
    # TgCb: .text, .value, .msgId, .chatId, .userId, .userName, .timestamp, .raw_update
    if TgCb.value == "red":
        bot.send_msg(f"You pressed {TgCb.text} -> {TgCb.value}")
```

### API request handler

Register a POST route; the embedded server starts automatically when the first route is registered.

```python
@bot.on_api_req("/action", args=["chat", "msg"])
def action():
    # TgArgs.get(key, default)
    chat_id = TgArgs.get("chat")
    msg = TgArgs.get("msg")
    bot.send_msg(msg, chat=chat_id)
```

**TReq** – client to call these endpoints:

```python
from TgrEzLi import TReq

TReq("/action").host("127.0.0.1").port(9999).arg("chat", "chat1").arg("msg", "Hello!").send()
# or
TReq("/action").body({"chat": "chat1", "msg": "Hello!"}).send()
```

`.host()`, `.port()`, `.timeout(seconds)` are optional (defaults: localhost, 9999, 30s). `.send()` returns a `requests.Response`; on failure raises **`TgrezliRequestError`**.

CamelCase decorator aliases: `onMessage`, `onCommand`, `onCallback`, `onApiReq`.

---

## Data interfaces (handler context)

Use only **inside** the corresponding handler; otherwise `TgMsg` / `TgCmd` / `TgCb` / `TgArgs` raise if accessed.

| Proxy | Handler | Main fields |
|-------|---------|-------------|
| **TgMsg** | `on_message` | `text`, `msgId`, `chatId`, `userId`, `userName`, `timestamp`, `raw_update` |
| **TgCmd** | `on_command` | `command`, `args`, `msgId`, `chatId`, `userId`, `userName`, `timestamp`, `raw_update` |
| **TgCb** | `on_callback` | `text`, `value`, `msgId`, `chatId`, `userId`, `userName`, `timestamp`, `raw_update` |
| **TgArgs** | `on_api_req` | `TgArgs.get(key, default)` for POST body |

All IDs/names are available as both **snake_case** (e.g. `msg_id`) and **camelCase** (e.g. `msgId`) on the data objects.

---

## Stopping

**`stop()`** stops polling and the API server; if **`run()`** was blocking, it returns. Call **`stop()`** via **Ctrl+C** (SIGINT is handled) or from another thread. After that, to use the bot again you must call **`connect()`** (or **`login()`**) again.

---

## Project layout

- **`TgrEzLi`** – main package
  - **`TEL`** – bot class (core)
  - **`TgMsg`, `TgCmd`, `TgCb`, `TgArgs`** – handler context proxies (from `TgrEzLi.models`)
  - **`TReq`, `TgrezliRequestError`** – HTTP client for the embedded API
  - **`TgrezliConfig`** – config dataclass (from `TgrEzLi.types`)
- **`TgrEzLi.crypto`** – **`CredentialManager`** (uses PyCypherLib): `signup(token, chat_dict, password)`, `login(password)`.

Logging is done with **ezlog** on the console; if `save_log` is true, the same messages are appended to the configured log file.

---

## Security and robustness

- **API server**: POST body size limited (default 1 MiB); invalid JSON returns 400; unknown route 404. No built-in authentication – protect the server (e.g. firewall, reverse proxy, or add your own auth in routes).
- **Credentials**: Stored in an encrypted file when using `signup`/`login` (PyCypherLib); decryption only with the correct password. Keep the file and password secure.
- **Handlers**: User code runs in daemon threads; exceptions are logged and do not crash the bot.

---

## License

MIT License. Copyright (c) eaannist.