Metadata-Version: 2.4
Name: velora-api-client
Version: 0.5.8
Summary: Python client for Velora and Bonfire: instances, sign-in tickets, scopes, REST, gateway, bots, webhooks and signing
Author: Ikelene
License-Expression: MIT
Project-URL: Documentation, https://velora.ikelene.net/guides/velora-api
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: realtime
Requires-Dist: websockets<18,>=14; extra == "realtime"
Provides-Extra: signing
Requires-Dist: cryptography>=42; extra == "signing"
Provides-Extra: all
Requires-Dist: websockets<18,>=14; extra == "all"
Requires-Dist: cryptography>=42; extra == "all"
Dynamic: license-file

# Velora API client for Python

Velora is the identity and platform API: accounts, direct messages, groups, friends,
credits, instance tickets, signing keys and hosting. **Bonfire** is the chat system. Every
Bonfire server is an **instance** at its own web address (the main community is
`https://chat.ikelene.net`; Velora-hosted ones are `https://<slug>-bonfire.ikelene.net`;
you can also self-host). One instance holds one community. This package talks to both.

It supports Python 3.10+ and exposes every documented REST operation as a named
method, plus a generic request method. REST needs only the Python standard
library. The optional `realtime` extra installs `websockets`; the optional
`signing` extra installs `cryptography` for message signing and verification.

## Install

This release is hosted by Velora, not on PyPI. Install the actual wheel URL:

```sh
python -m pip install "https://velora.ikelene.net/assets/api/velora_api_client-0.5.8-py3-none-any.whl"
```

For realtime and signing, install the wheel first, then:

```sh
python -m pip install "velora-api-client[realtime]==0.5.8"   # websockets
python -m pip install "velora-api-client[signing]==0.5.8"    # cryptography
```

Or download the source zip, extract it, and run `python -m pip install ".[realtime]"`
inside the folder containing `pyproject.toml`. `requirements.txt` installs the
optional websocket dependency only, not the Velora package itself.

## Sign in, and what scopes mean now

```python
from velora_api import Client, login, scope_mask

token = login(project_name="My Python client", scopes=scope_mask("PROFILE_READ", "CHAT"))
velora = Client(token)
print(velora.scope_names())          # what the person actually allowed
print(velora.bonfire_instances())    # the Bonfire servers saved on this account
```

The browser shows a consent screen. **Scopes are requested, never required**: the person
can untick any of them (the risky ones start unticked), so a token can come back with
fewer scopes than you asked for. Ask only for what you use, then check:

```python
velora.has_scope("MEDIA_UPLOAD")     # True / False  (names, labels such as "Upload Media", or bits)
velora.granted_scopes()              # the usable mask (approved and not switched off by an admin)
velora.require_scope("CHAT")         # raises ScopeError with a clear message
```

A call without the scope raises `ScopeError` (a subclass of `APIError`) with `scope`,
`label` and `disabled_by_admin`, so you can show "log in again to use this" instead of
crashing. `login()` never asks for a password. Project names that start with
`Bonfire (` are reserved for the official apps. `optional_scopes=` is still accepted
and merged into `scopes`. To print the URL yourself use `login(..., on_url=print)`.
Do not print the returned token. For an existing token use `Client(os.environ["VELORA_TOKEN"])`.

| Scope | Bit | Default on the consent screen | Bonfire instance scope |
|---|---|---|---|
| View Basic Profile | 1 | on | |
| View Sensitive Info | 2 | off | |
| Project Auth | 4 | on | |
| Modify Settings | 8 | off | |
| Read Storage / Write Storage | 16 / 32 | on / off | |
| Read Instant Messages / Send Instant Messages | 64 / 128 | on | |
| View Friends / Manage Friends | 256 / 512 | on / off | |
| Spend Credits | 1024 | off | |
| Chat Messaging | 2048 | on | `chat` (needed to sign in to an instance) |
| Upload Media | 4096 | on | `media` (uploads) |
| View Subscriptions | 8192 | on | |
| Velora Voice | 16384 | on | `voice` |
| Sign Messages | 32768 | off | `sign` (signed messages) |

`velora_api.SCOPES`, `SCOPE_LABELS`, `DEFAULT_OFF`, `INSTANCE_SCOPES`,
`scope_mask()`, `scope_names()` and `has_scope()` give you the same table in code.

## Bonfire servers (instances)

```python
from velora_api import Client, Instance

velora = Client(token)                              # needs Chat (2048)
chat = Instance("https://chat.ikelene.net")
info = chat.discover()                              # /.well-known/bonfire, no login
print(info["name"], info["accounts_mode"], info["build"]["version"], info["read_only"])
chat.login_with_velora(velora, sign="auto")         # Velora-login instances
# chat.login("name", "password")                    # local-account instances
# chat.register("name", "password")

print(chat.scopes)                                  # instance scopes of this session: chat, voice, media, sign
channel = chat.channel_id("#general")
for message in chat.messages(channel, limit=20)["messages"]:
    print(message["author"]["username"], message["body"])
chat.send_message(channel, "Hello from Python!")
```

`login_with_velora` asks Velora for a short ticket made for that one instance and
trades it at `POST /api/v1/auth/velora`. Your Velora token never leaves Velora. The
ticket carries the instance scopes the person allowed (`chat`, `voice`, `media`, `sign`);
`sign="auto"` signs only when Sign Messages was allowed, `sign=True` raises `ScopeError`
if not. The instance session lasts 30 days from last use.

Discovery helpers: `accounts_mode`, `features`, `build` (version, commit, build_id),
`read_only`, `instance_info()` (`GET /api/v1/instance/info`, admins also get runtime, stats
and `update`) and `update_available()`. `Client.server_latest()` returns the official
build instances compare with.

Useful methods: `servers()` / `joined_communities()`, `channels()`, `members()`,
`send_message()`, `edit_message()`, `delete_message()`, `react()`, `pin()`, `search()`,
`join()`, `invites()`, `create_invite(expires_in=, max_uses=)`, `delete_invite()`,
`join_invite(code)`, `invite_info(code)`, `join_requests()`, `accept_join_request(uid)`,
`create_channel()`, `set_channel_permissions()`, `kick()`, `webhooks()`,
`create_webhook()`, `bots()`, `create_bot()`, `backups()`, `create_backup()`, `settings()`,
`identities(ids)`, `set_notify()`. Every route also has a named method
(`get_api_v1_servers_by_sid`) and `instance.call(...)`. IDs are strings; users look like
`v:123`, `l:7`, `b:3`.

### Your servers as one object

```python
from velora_api import Bonfire

bonfire = Bonfire(velora, sign="auto")
bonfire.join_default()                              # the official server, once per account
for item in bonfire.communities():                  # one entry per saved server
    print(item["instance"], (item["community"] or {}).get("name"), item["error"])
bonfire.add("https://camp-bonfire.ikelene.net")     # saved on the account, follows the person between devices
```

A server that is down, banned, in local-account mode, or where Chat was declined is
reported in `error` (`unreachable`, `local_accounts`, `chat_scope_missing`, an API code);
it never stops the others.

### Uploads and media import

```python
uploaded = chat.upload("picture.png", cid=channel)                 # needs the media scope
saved = chat.import_media("https://example.com/cat.png", cid=channel)   # the instance fetches an https URL
chat.send_message(channel, "A picture", attachments=[uploaded, saved])
```

A session without the `media` scope raises `ScopeError` before anything is sent.
Uploads also need the community's `allow_uploads` and the channel's Attach permission.

### Signing and verifying

```python
chat.login_with_velora(velora, sign=True)           # account key from Velora, kept in memory
sent = chat.send_message(channel, "Signed")
ok, reason = chat.verify_message(sent["message"])   # you check it, not the instance's `verified` flag
```

Signing is optional on every velora-mode instance and never required. It needs the
Sign Messages scope and the `signing` extra. Signatures follow protocol section 8, including
`url:` entries for link-only GIFs. `verify_message` reads the public key from Velora and
checks owner, revocation time and the Ed25519 signature.

### Webhooks need no login

```python
from velora_api import execute_webhook
execute_webhook("https://chat.ikelene.net/api/v1/webhooks/ID/TOKEN", "Build finished")
```

### Bots

An **instance bot** is created on one instance (`chat.create_bot("pingbot")` returns the
token once) and uses a bot token. REST is Discord-style at `/bot/v1` with numeric ids, the
bot gateway is `/bot/gateway`:

```python
bot = Instance("https://chat.ikelene.net", bot_token=os.environ["BONFIRE_BOT_TOKEN"])
async with bot.bot_gateway() as gateway:
    async for event in gateway:                    # {"type": "MESSAGE_CREATE", "data": {...}}
        ...
bot.bot_send("NUMERIC_CHANNEL_ID", "pong")
```

A **Velora developer bot** lives on your Velora account and can be listed in the bot
gallery (Discovery > Bots) so others add it to their servers. Each instance makes its own
bot account for it, but the token is the same. A bot program serves all of them:

```python
from velora_api import BotProgram

async with BotProgram(os.environ["VELORA_BOT_TOKEN"]) as bot:   # GET /bonfire/bots/self/installs
    async for event in bot:                                      # {"instance", "type", "data", "seq"}
        if event["type"] == "MESSAGE_CREATE" and event["data"]["content"] == "!ping":
            await bot.send(event["instance"], event["data"]["channel_id"], "pong")
```

It opens one gateway per install, re-reads the install list every 5 minutes, and keeps
going when one instance fails. Gallery owner calls: `list_gallery()`, `save_gallery_listing()`,
`submit_to_gallery()`, `gallery_status()`, `withdraw_from_gallery()`, `gallery_ticket()` and
`Instance.add_gallery_bot(ticket)`.

### User gateway

```python
chat.login_with_velora(velora)
async with chat.gateway() as gateway:
    async for event in gateway:
        if event["type"] == "chat_message":
            print(event["message"]["body"])
    # or: @gateway.on("chat_message") ... then: await gateway.run()
await_ack = await gateway.send_message(channel, "hi")   # waits for chat_ack
```

It reconnects with capped backoff and jitter (1s to 30s), pings every 25 s, and stops on
close codes 4001 and 4003. There is no replay: after `gateway_ready` follows a drop,
refetch history over REST and dedupe by id.

### Other Velora-side Bonfire helpers

`Client`: `instance_token(url)`, `signing_key()`, `bonfire_instances()`,
`set_bonfire_instances(urls)`, `sync_bonfire_instances(urls)`, `bonfire_default_instance()`,
`bonfire_default_joined()`, `bonfire_instance_prefs()`, `bonfire_directory(q)`,
`bonfire_profiles(ids)`, `bonfire_key(kid)`, `bonfire_jwks()`, `bonfire_scopes()`,
`bonfire_import_platforms()` / `start_bonfire_import()` (Discord and Fluxer bundles),
and hosting: `hosting_plans()`, `hosting_servers()`,
`create_hosting_server(slug, name, plan, weeks=)`, `buy_hosting_weeks(slug, weeks)`,
`backup_hosting_server(slug)`, `hosting_backups()`, `upload_hosting_backup(...)`,
`delete_hosting_server(slug)`. Plus is prepaid in weeks and never renews by itself; it is
free for Velora Pro. Examples are in `examples/bonfire_*.py`.

## Velora REST methods

Velora-hosted chat servers (`/chat/servers`, `/chat/channels` and so on) are
legacy and read-only now (writes answer `server_read_only` with `moved_to`). Direct
messages, groups, friends, storage, credits and presence are unchanged.

```python
velora.send_dm("someone", "Hello from Python!")
velora.set_activity("My game", state="In the menu", ttl=600)
velora.clear_activity()
```

The old `servers()`, `server()`, `messages()` and `send_message()` helpers on `Client`
still call the legacy routes (they now emit a `DeprecationWarning`); use `Instance` for
new work.

`send_dm(username, message)` requires Send Instant Messages (128). Storage uses Read (16) /
Write (32); its values are strings. Use `json.dumps()` / `json.loads()` for structured data.

Every operation in the downloadable catalogues (Velora on `Client`, instances on `Instance`)
has a named method. For example:

```python
chat.post_api_v1_servers_by_sid_roles(
    sid="YOUR_SERVER_SID", json={"name": "Member", "permissions": 257}
)
chat.call("get_api_v1_servers_by_sid", sid="YOUR_SERVER_SID")
velora.call("get_bonfire_hosting_plans")
velora.request("GET", "/bonfire/instances")
```

Use `query={...}` for queries, `json={...}` for JSON, or `data={...}, files={...}` for
multipart. Path parameters are URL encoded; optional parameters may be omitted. The API
checks token scopes, server/channel roles, rank and privacy separately. File values can
also be `(filename, bytes_or_file_object, mime_type)`. This client buffers multipart data
with a 64 MiB limit; server limits can be smaller.

## Errors

```python
from velora_api import APIError, ScopeError, TransportError

try:
    chat.upload("picture.png")
except ScopeError as exc:
    print("Log in again and keep this permission ticked:", exc.label or exc.error)
except APIError as exc:
    print(exc.status, exc.error, exc.retry_after, exc.moved_to)
except TransportError:
    print("Network error. Check whether a write completed before retrying.")
```

The library does not retry requests automatically. Handle 429 using Retry-After or
`retry_after` when present, with backoff. Error payloads are available as `exc.payload`.
Some endpoints return empty 204, text or binary responses; use `raw=True` for bytes.
Redirects are rejected so credentials are never forwarded.

## Realtime user client (Velora)

```python
import asyncio
import os
from velora_api import RealtimeClient

async def main():
    async with RealtimeClient(os.environ["VELORA_TOKEN"]) as socket:
        await socket.set_activity("My Python app", state="Connected")
        async for event in socket:
            if event.get("type") in ("im_received", "presence_update"):
                print(event)

asyncio.run(main())
```

This is Velora's own socket: presence, direct messages, groups, calls. Entering the context
waits for `auth_success`, checks `tos_required`, starts the reader and sends `chat_hello`
when the Chat scope is granted. Its `chat_*` frames only concern legacy Velora-hosted
servers; Bonfire chat uses the instance gateway above. `rpc()` is only for frames with nonce
replies. On disconnect, reconnect, authenticate, refetch REST state and rebuild presence.
There is no durable websocket replay.

This package transports voice signaling but does not implement WebRTC, audio
capture/playback or a desktop UI. See the voice guide for a WebRTC implementation,
including two peer connections and subscription snapshots.

## Legacy Velora bots and webhooks

These Velora-hosted routes are read-only now; use the instance bots and webhooks above.

```python
bot = Client(os.environ["VELORA_BOT_TOKEN"], auth_scheme="Bot")
me = bot.get_chat_api_v1_users_me()
```

## Game servers (Bonfire Link, Minecraft)

On a Bonfire instance the game plugin connects out to the instance
(`bonfire server https://chat.example` in the game console); an admin picks it from the
connected list:

```python
for server in chat.game_connected()["instances"]:          # waiting to be linked (Manage server)
    print(server["iid"], server["pairing_tag"], server["motd"])
chat.game_link(iid, cid=GAME_CHANNEL_CID)
print(chat.game_status(GAME_CHANNEL_CID))
```

`Client.game_*` still exist for the legacy Velora routes. Console commands may require the
website or the Bonfire app (`console_needs_bonfire`).

## Bot payments (0.5.0)

A bot can ask a Velora user for credits. The bot authenticates with its **bot token**; the buyer
opens the returned `pay_url`, signs in with their normal login (no scopes, no app authorization)
and presses Pay. The credits go to the bot's owner. There is no webhook, so you poll.

```python
from velora_api import BotClient, is_paid, OrderConflict, BuyerError, PaymentError

bot = BotClient(os.environ["BOT_TOKEN"])
request = bot.create_payment_request("alice", 20, "ord-1001", description="Premium pack",
                                     return_url="https://shop.example/done", expires_in=900)
print(request["pay_url"])                         # send the buyer here, do not log it publicly
final = bot.wait_for_payment(request["id"], timeout=900, poll=3)   # PaymentTimeout when still pending
if is_paid(final) and final["amount"] == 20:      # fulfil ONLY when paid; check amount and buyer id
    ...
```

`get_payment_request(id)`, `find_payment_request(order_id)` (None if unknown),
`list_payment_requests(status=None, limit=None)` and `cancel_payment_request(id)` complete the set.
Repeating `create_payment_request` with the same order id, buyer and amount returns the existing request
(`idempotent_replay` is true); a different buyer or amount raises `OrderConflict`. Errors are
`PaymentError` subclasses (still `APIError`): `InvalidBotToken`, `MerchantUnavailable`,
`InvalidPaymentRequest`, `BuyerError`, `OrderConflict`, `PaymentInProgress`, `NotCancellable`,
`PaymentNotFound` and `PaymentRateLimited`; `error.error` keeps the server code. Run this on a server:
**never put a bot token in a Scratch/PenguinMod project, web page or app** (the PenguinMod extension has no
payment blocks for that reason). See `examples/bot_payments.py` and the Bot payments section of the guide.

Bot presence: a bot is online in a community while it holds an authenticated `/bot/gateway` connection.
Member objects from `Instance.members(...)` carry `presence` (`"online"` or `"offline"` for bots, `null` for
people) and the instance websocket (`InstanceGateway`) delivers `chat_presence` events; the library passes these through unchanged.
`Client.bot_installs()` includes staff test installs with `test: true` (not counted as real installs; skip them).

## Notifications (0.5.7)

Friend requests, credits, store sales, badges, membership, hosting and messages from the Velora team are
notifications (the bell on the site and in Ember). System DMs are gone: those messages are notifications now.

```python
page = velora.notifications(unread=True, limit=20)      # {"notifications": [...], "next_cursor": ..., "unread": n}
for n in page["notifications"]:
    print(n["category"], n["title"], n["body"])          # plain text, **bold** is the only markup
velora.mark_notifications_read([n["id"] for n in page["notifications"]])   # or mark_notifications_read(all=True)
print(velora.unread_notifications())
for category in velora.notification_settings():          # locked ones (system, moderation) are always on
    print(category["key"], category["list"], category["email"], category["locked"])
velora.set_notification_settings({"friends": {"email": False}})
```

`mark_notifications_unread(ids)` and `delete_notification(id)` complete the set; the generated methods are
`get_account_notifications`, `get_account_notifications_unread_count`, `post_account_notifications_read`,
`post_account_notifications_unread`, `delete_account_notifications_by_id`, `get_account_notification_settings`
and `patch_account_notification_settings`. Live changes arrive on the user websocket:

```python
async with RealtimeClient(os.environ["VELORA_TOKEN"]) as socket:
    async for event in socket.notification_events():     # notification_new / notifications_updated
        print(event["type"], event["unread"])
```

`notification_new` carries `notification` (same shape as the list) and `unread`; `notifications_updated` carries
`unread` and one of `read` (ids or `"all"`), `unread_ids` or `deleted`. The older `/notifications` routes still
exist for the Scratch extension. Staff can send with `post_admin_notifications_send`.

## Bot aliases (0.5.8)

A bot can post a message under another name and avatar. The message still belongs to the bot (mentions,
moderation and reports use the real bot); clients show the alias with a mask icon and the tooltip
"Posted by BOT as ALIAS". Only bot tokens may set an alias; a user or session token gets `403 alias_bots_only` (`APIError`), also for `alias=None` and on the gateway `chat_send`.

```python
from velora_api import Instance, make_alias

bot = Instance("https://chat.ikelene.net", bot_token=os.environ["BOT_TOKEN"])
sent = bot.bot_send("CHANNEL_ID", "Welcome in!", alias={"name": "Narrator", "avatar_url": "https://example.com/n.png"})
print(sent["alias"])                        # {"name": "Narrator", "avatar_url": "https://example.com/n.png"}
bot.bot_edit("CHANNEL_ID", sent["id"], alias=make_alias("Storyteller"))   # change it
bot.bot_edit("CHANNEL_ID", sent["id"], alias=None)                        # clear it
```

`name` is 1-32 characters with no line breaks and `avatar_url` is optional (`None` or `""` = no avatar), `https://` only, at most 500
characters, no `user:pass@`, no whitespace; leaving `alias` out of `bot_edit` keeps the current one; `make_alias` checks both before the request (`ValueError`). `alias=` is also accepted by
`BotProgram.send(...)` (through its fields), `Instance.send_message`, `Instance.edit_message` and
`InstanceGateway.send_message`. Messages are plain dicts in this library, so read `message["alias"]`, which is
`None` or `{"name", "avatar_url"}`; `message["author"]` is always the real bot.

## Complete reference

https://velora.ikelene.net/guides/velora-api

The source catalogue records the live API on 2026-10-19, including staff routes. Not every
input has a strict JSON Schema and not every privileged operation is tested live. The
server remains authoritative. Internal bridge/payment integration credentials and
deployment callbacks are not public client APIs.

## Changes in 0.5.8

- Bot aliases: `alias=` on `Instance.bot_send`, `Instance.bot_edit` (`alias=None` clears), `Instance.send_message`,
  `Instance.edit_message` and `InstanceGateway.send_message`, plus `make_alias(name, avatar_url=None)` which checks
  the limits. `bot_edit` now takes `content` as optional so an alias can be changed alone. Nothing else changed.

## Changes in 0.5.7

- Notifications: new generated methods for `/account/notifications` (list, unread count, read, unread, delete),
  `/account/notification-settings` (GET, PATCH) and the staff `POST /admin/notifications/send`, helpers
  `notifications()`, `unread_notifications()`, `mark_notifications_read()`, `mark_notifications_unread()`,
  `delete_notification()`, `notification_settings()`, `set_notification_settings()`, and
  `RealtimeClient.notification_events()` with `NOTIFICATION_EVENT_TYPES` (`notification_new`, `notifications_updated`).
- System DMs (`from_system` messages) are no longer created; those messages are notifications now.

## Changes in 0.5.1

- New `get_chat_media_limits()`: the real upload limits (`server_mb` for files sent in a conversation, `saved_mb` for files kept
  on your Velora account). `get_chat_unfurl()` can now return `payment` cards for Velora payment links. Nothing else changed.

## Changes in 0.5.0

- New `BotClient` (bot token) with bot payment helpers: `create_payment_request`,
  `get_payment_request`, `find_payment_request`, `list_payment_requests`, `cancel_payment_request`,
  `wait_for_payment`, `is_paid`, plus the `PaymentError` family. The four `/bot/payment-requests`
  routes are also generated methods (`post_bot_payment_requests`, ...).
- Docs for bot presence and staff test installs (`test: true`). No behaviour changes elsewhere.

## Changes in 0.4.0

- Scopes are only requested: `login(scopes=...)` sends them as an optional request, the new
  `scopes` module and `Client.granted_scopes()/has_scope()/require_scope()` report what the
  person allowed, and calls without a scope raise `ScopeError` (an `APIError` subclass).
- Instance scopes (`chat`, `voice`, `media`, `sign`) from the ticket: `Instance.scopes`,
  `has_scope()`, `sign="auto"`, early `ScopeError` for uploads.
- New: `Bonfire` (your saved servers), `BotProgram` (a bot on every instance it is
  installed on), `verify_message`, invites, join requests, media import, channel and role
  helpers, game/bridge/admin helpers, instance info and build, Velora helpers for profiles,
  keys, JWKS, scopes, import jobs, default server and push preferences.
- 458 Velora and 169 instance operations as named methods.
- Legacy `Client.servers()/server()/messages()/send_message()` warn that Velora-hosted chat is read-only.
