Metadata-Version: 2.4
Name: luma-bot
Version: 1.9.0
Summary: Official Python SDK for Luma bots, voice assistants, UI components, and Sign in with Luma.
Author: BlackBullNetwork
License-Expression: MIT
Project-URL: Homepage, https://luma.blackbullnetwork.eu
Project-URL: Documentation, https://luma.blackbullnetwork.eu/app/developers?tab=docs
Project-URL: Repository, https://blackbullnetwork.eu
Project-URL: Issues, https://luma.blackbullnetwork.eu/app/support
Keywords: Luma,bot,SDK,voice,TTS,OAuth,Socket.IO
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: python-socketio[asyncio_client]<6,>=5.11
Requires-Dist: SpeechRecognition<4,>=3.17
Dynamic: license-file

# Luma Bot SDK (`luma_bot`)

A Python SDK for creating bots on Luma.

## Install

```bash
pip install luma-bot
```

## Minimal bot

```python
import os

import luma_bot
from luma_bot.ext import commands

bot = commands.Bot()

@bot.event
async def on_ready():
    print(f"Logged in as {bot.user}")

@bot.command(description="Say hello")
async def hello(ctx: luma_bot.Context):
    await ctx.send(f"Hello, {ctx.author.display_name}!")

bot.run(os.environ["LUMA_BOT_TOKEN"])
```

`commands.Bot()` automatically connects to the official Luma API and realtime gateway. Normal bot developers do **not** configure a base URL or `LUMA_URL`; the endpoint is built into the SDK. Only self-hosted/test deployments need to pass `base_url=` explicitly.

## Quick start

Create a bot in **Luma → Developer Console**, copy the token shown once, then:

```bash
python -m pip install --upgrade luma-bot edge-tts
export LUMA_BOT_TOKEN="luma_..."   # Linux/macOS
# Windows PowerShell: $env:LUMA_BOT_TOKEN="luma_..."
python your_bot.py
```

Your code only needs `commands.Bot()`; the official Luma service address is already part of the SDK.

## Included examples

- `examples/minimal_bot.py` — smallest token + slash-command starter.
- `examples/basic_bot.py` — commands, events, timers, UI, server data, voice, speech recognition, and Edge-TTS.
- `examples/components_bot.py` — buttons, views, and rich cards.
- `examples/voice_assistant.py` — speech transcript → Edge-TTS assistant.
- `examples/voice_bot.py` — voice join/playback basics.

Every normal example uses `commands.Bot()` with the official Luma connection built in.

## Authentication

`bot.run(token)` validates the token through `GET /api/v1/bot/me` before opening the Socket.IO gateway. Send bot tokens as `Authorization: Bearer luma_...`; the platform also accepts the older `Bot` prefix while installations upgrade. User IDs, application IDs, account sessions, revoked tokens, disabled bots, and banned bots are rejected.

## Supported bot REST API

The SDK is intentionally limited to routes protected by the `botToken` security scheme in Luma OpenAPI:

| Method | Route | SDK behavior |
|---|---|---|
| `GET` | `/api/v1/bot/me` | Token validation and bot identity |
| `PATCH` | `/api/v1/bot/presence` | `bot.change_presence()` |
| `PUT` | `/api/v1/bot/commands` | Automatic slash-command synchronization |
| `POST` | `/api/v1/bot/channels/{channelId}/messages` | `ctx.send()` and `bot.send_message()` |
| `POST` | `/api/v1/bot/messages/{messageId}/reactions` | `ctx.react()` and `bot.add_reaction()` |
| `DELETE` | `/api/v1/bot/messages/{messageId}/reactions` | `ctx.remove_reaction()` and `bot.remove_reaction()` |
| `GET` | `/api/v1/bot/communities` | `bot.fetch_communities()` (installed servers only) |
| `GET` | `/api/v1/bot/communities/{communityId}` | `bot.fetch_community()` (installed servers only) |
| `GET` | `/api/v1/bot/communities/{communityId}/members` | `bot.fetch_members()` / `bot.fetch_users()` |
| `GET` | `/api/v1/bot/communities/{communityId}/members/{memberId}` | `bot.fetch_member()` |
| `GET` | `/api/v1/bot/communities/{communityId}/users/{userId}` | `bot.fetch_user()` (only in that server) |
| `GET` | `/api/v1/bot/communities/{communityId}/channels` | `bot.fetch_channels()` |
| `GET` | `/api/v1/bot/communities/{communityId}/roles` | `bot.fetch_roles()` |
| `POST` | `/api/v1/bot/communities/{communityId}/invites` | `bot.create_invite()` |
| `PUT` / `DELETE` | `/api/v1/bot/communities/{communityId}/members/{memberId}/roles/{roleId}` | `ctx.add_role()`, `bot.add_role()`, `bot.remove_role()` |
| `PATCH` | `/api/v1/bot/communities/{communityId}/members/{memberId}/timeout` | `ctx.timeout()` / `bot.timeout_member()` |
| `DELETE` | `/api/v1/bot/communities/{communityId}/members/{memberId}` | `ctx.kick()` / `bot.kick_member()` |
| `PUT` | `/api/v1/bot/communities/{communityId}/members/{memberId}/ban` | `ctx.ban()` / `bot.ban_member()` |

Public diagnostics are also available:

```python
live = await bot.is_service_live()
ready = await bot.is_service_ready()
```

The SDK does not use user-session, CSRF-protected, developer-portal, or admin routes.

## Reactions

```python
@bot.command(description="Celebrate this message")
async def celebrate(ctx):
    await ctx.react("🎉")
```

Bots can only react in servers where they are installed and have the **Add Reactions** permission.

## Commands

```python
@bot.command(description="Add two numbers")
@commands.describe(first="First number", second="Second number")
async def add(ctx, first: int, second: int):
    await ctx.send(str(first + second))
```

Type annotations become command option types:

- `str` -> `string`
- `int` -> `integer`
- `float` -> `number`
- `bool` -> `boolean`
- parameters with defaults are optional
- `str | None` is optional

### discord.py-style command tree

Luma uses slash commands, so `bot.tree` is the familiar way to declare
and synchronise them. `commands.Bot.command()` and `bot.tree.command()` both
register the same command type; automatic syncing is enabled by default.

```python
from luma_bot.ext import commands

bot = commands.Bot()

@bot.tree.command(description="Check whether the bot is online")
async def ping(ctx):
    await ctx.reply("Pong!")

@bot.event
async def on_ready():
    # Optional: use this when commands are changed while the process is running.
    await bot.tree.sync()
```

The client also provides `bot.add_command()`, `bot.get_command()`, and
`bot.remove_command()` for dynamic command registration.

## Typing indicators

Make the bot visibly type while it is preparing a response. A single await
shows a short indicator; an async context keeps it visible until the work is
done and clears it immediately afterwards.

```python
@bot.tree.command(description="Show server information")
async def server(ctx):
    async with ctx.typing():
        data = await bot.fetch_community(ctx.community.id)
    await ctx.reply(f"{data['community']['name']} is ready.")

# Or show a brief indicator from any background task:
await bot.typing(channel_id)
```

The Luma client groups simultaneous typers into a clear “Multiple people
are typing…” line, including bots.

## UI components

```python
from luma_bot import ui

class Menu(ui.View):
    @ui.button(label="Click", custom_id="menu:click", style=ui.ButtonStyle.primary)
    async def click(self, interaction, button):
        await interaction.respond("Clicked!")

@bot.command(description="Show UI")
async def menu(ctx):
    await ctx.send("Choose:", view=Menu())
```

Use `bot.add_view(Menu())` during `on_ready` to restore a reusable view at
startup. In callbacks, both `await interaction.respond("…")` and the familiar
`await interaction.response.send_message("…")` are supported.

## Rich embeds, images, video, and player previews

Use `ui.Embed` for an announcement or profile-style card, then combine it with
normal interactive controls or external `ui.LinkButton` actions. Images, direct
MP4/WebM/Ogg video, and allowlisted YouTube, Vimeo, or Twitch player URLs are
rendered safely in the client.

```python
from luma_bot import ui

@bot.command(description="Show the community profile card")
async def profile(ctx):
    card = ui.Embed(
        title="BlackBull310",
        description="Community profile and live status.",
        color="#2f80ed",
        image_url="https://cdn.example.com/profile-card.png",
        image_alt="BlackBull310 profile card",
        footer="Luma profile service",
    ).add_field("Level", "5 / 100", inline=True).add_field("Created", "2021-05-18", inline=True)

    links = ui.View().add_item(ui.LinkButton(label="Twitch", url="https://twitch.tv/example"))
    await ctx.send("", components=[card.to_dict()], view=links)
```

For an iframe player, set `iframe_url` to an HTTPS YouTube, Vimeo, or Twitch
embed URL. For uploaded platform media, `image_url`, `thumbnail_url`,
`video_url`, and `video_poster_url` can use a Luma `/uploads/...` path.

## Replying to messages

User replies and bot replies share the same quoted-message display. In a bot
command, call `ctx.reply()` to quote the message that invoked the command.
Interaction responses automatically quote the message containing the clicked
button or select menu.

```python
@bot.command(description="Reply in context")
async def hello(ctx):
    await ctx.reply("Hey! I am replying directly to your message.")
```

## Voice gateway

```python
await bot.voice.join(channel_id)
await bot.voice.play(channel_id, audio_url, title="Music")
# For generated speech, install edge-tts and use voice.play_bytes().
# See examples/voice_assistant.py for the complete helper.
await bot.voice.leave(channel_id)
```

Voice operations use the realtime gateway rather than a REST route. `audio_url`
must be an HTTPS URL or a Luma `/uploads/...` file that each listener's
browser can reach. For generated TTS, the examples use `edge-tts` in the bot process and send the resulting MP3 with `bot.voice.play_bytes()`, so
there is no temporary MP3 to host.

### Voice recognition / voice assistants

Enable **Voice recognition** and **Text-to-Speech** for the application in
Developer Console, then add the `voice_transcript` event intent. When the bot is
inside a voice or stage channel, members see a clear **bot listening** indicator
and can turn voice-bot recognition off locally. Luma forwards short consented PCM speech utterances to the connected bot SDK; `luma_bot` 1.8+ transcribes them and dispatches `on_voice_transcript`. Luma does not persist the recognition audio.

```python
@bot.event
async def on_voice_transcript(event):
    if not event.get("final"):
        return
    heard = event.get("text", "").strip().lower()
    channel_id = event["channel"]["id"]
    speaker = event["speaker"]["display_name"]
    if heard == "hello":
        await speak_with_edge_tts(channel_id, f"Hello {speaker}!")
```

Call `await bot.voice.listen(channel_id, enabled=False)` to pause transcript
delivery without leaving the channel.

## Events

```python
@bot.event
async def on_connect():
    print("Gateway connected")

@bot.event
async def on_ready():
    print("Bot ready")

@bot.event
async def on_command_error(ctx, error):
    await ctx.send(f"Error: {error}")

@bot.event
async def on_interaction(interaction):
    print(interaction.custom_id)
```

Installed bots also receive message events without polling:

```python
@bot.event
async def on_message(message):
    if message.author.bot:
        return
    print(message.channel_id, message.author.username, message.content)
```

For future platform events not yet represented by a typed callback, use
`on_raw_event(event_type, payload)`. The SDK dispatches `on_message_create` in
addition to `on_message` for message-created events.

For background tasks that must not run before the gateway is ready, use the
same lifecycle pattern as discord.py:

```python
@my_task.before_loop
async def wait_for_gateway():
    await bot.wait_until_ready()
```

## Timers and scheduled jobs

```python
from luma_bot import tasks

@tasks.loop(minutes=15)
async def refresh_status():
    await bot.change_presence(
        status="online",
        activity_type="watching",
        activity_text="new community activity",
        activity_color="#725cff",
    )

@bot.event
async def on_ready():
    if not refresh_status.is_running():
        refresh_status.start()
```

Set `status` to `online`, `idle`, `dnd`, `invisible`, or `offline`. Supported
activity types are `playing`, `streaming`, `listening`, `watching`, and
`custom`.

## Server data and roles

Bots can only inspect a server where they are installed. They cannot read data
from other servers, even if they know an ID.

```python
@bot.command(description="Show this server's member total")
async def stats(ctx):
    server = await bot.fetch_community(ctx.community.id)
    await ctx.reply(f"{server['stats']['members']} members")

@bot.command(description="Give a member a role")
async def give_role(ctx, member_id: str, role_id: str):
    await ctx.add_role(member_id, role_id)
    await ctx.reply("Role updated.")
```

`ctx.add_role()` requires the bot to have the server's **Manage Roles**
permission. `fetch_users()` returns ordinary dictionaries, so integrations can
use `for user in await bot.fetch_users(server_id): print(user["username"])`.

## Invites and moderation

Bots can create an invite when they have **Create Invites**:

```python
invite = await bot.create_invite(ctx.community.id, expires_in_hours=24, max_uses=25)
await ctx.reply(f"Invite created: {invite['token']}")
```

`timeout_member`, `kick_member`, `ban_member`, and role updates require the
matching server permission. They can only target members and roles below the
bot's highest assigned role; bots cannot act on the server owner or themselves.

## Sign in with Luma

Create an **OAuth application** in Developer Console, add each exact callback
URL, and keep the client secret only on your website's server. Luma uses
OAuth 2.1 authorization code flow with mandatory PKCE (S256), short-lived
access tokens, rotating refresh tokens, and a consent page.

```python
from luma_bot import OAuthClient

oauth = OAuthClient(
    client_id="luma_client_...",
    client_secret="luma_client_secret_...",
    redirect_uri="https://your-app.example/auth/luma/callback",
)

# Start login: store request.state and request.code_verifier in the user's
# temporary server-side session, then redirect them to request.url.
request = oauth.create_authorization_request(scopes=("identity", "email"))

# Callback: verify `state` first, then exchange the code using the verifier.
tokens = await oauth.exchange_code(code, request.code_verifier)
identity = await oauth.fetch_identity(tokens.access_token)
print(identity["display_name"])
```

Use `await oauth.refresh(tokens.refresh_token)` to renew an expired access
token and `await oauth.revoke(tokens.refresh_token)` when a user disconnects
their Luma account.

Documentation: https://luma.blackbullnetwork.eu/app/developers?tab=docs

## Complete Luma developer API

This SDK README documents the **19 bot-token REST operations** the Python bot runtime can call directly. Luma's complete supported developer contract contains **56 REST operations** and is documented in the main project at:

- `docs/DEVELOPER_API.md` — human-readable route, authentication, gateway, webhook, OAuth, App Directory, and voice/TTS reference.
- `docs/openapi.yaml` — machine-readable OpenAPI 3.1 contract (v0.8.0).
- Developer Console → **API Reference** — searchable in-app copy of the same supported route catalog.
- Help & Support → **API documentation** — searchable platform developer reference.

The additional operations cover OAuth, developer application management, App Directory installation/reviews, incoming webhooks, Luma client bot interactions, and service health. They are not exposed as ordinary `Bot` methods when they require a human session, OAuth client credential, or secret webhook URL.

After changing backend developer routes, run:

```bash
python tools/audit_developer_docs.py
```

The audit fails when the backend-supported developer contract, Developer Console route catalog, or OpenAPI developer markers drift apart.


### Voice transcript diagnostics

The bundled `examples/basic_bot.py` and `examples/voice_assistant.py` print every final speech transcript to the bot terminal. A successful event looks like:

```text
[VOICE] #General Voice | Robin: hello luma (language=en-US, confidence=92%)
```

Voice gateway methods (`join`, `listen`, `say`, `play`, and `leave`) wait for a server acknowledgement. Configuration errors therefore raise immediately instead of failing silently.
