nookara 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
nookara-0.8.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ikelene
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
nookara-0.8.0/PKG-INFO ADDED
@@ -0,0 +1,574 @@
1
+ Metadata-Version: 2.4
2
+ Name: nookara
3
+ Version: 0.8.0
4
+ Summary: Python SDK for Nookara: sign-in, profiles, friends, chat, credits and payments, storage, and bots.
5
+ Author: Ikelene
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://nookara.cc
8
+ Project-URL: Documentation, https://nookara.cc/guides/velora-api
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Provides-Extra: realtime
13
+ Requires-Dist: websockets<18,>=14; extra == "realtime"
14
+ Provides-Extra: signing
15
+ Requires-Dist: cryptography>=42; extra == "signing"
16
+ Provides-Extra: all
17
+ Requires-Dist: websockets<18,>=14; extra == "all"
18
+ Requires-Dist: cryptography>=42; extra == "all"
19
+ Dynamic: license-file
20
+
21
+ # Nookara for Python
22
+
23
+ ```sh
24
+ pip install nookara
25
+ pip install "nookara[realtime]" # websockets
26
+ pip install "nookara[signing]" # cryptography, for signed messages and encrypted DMs
27
+ ```
28
+
29
+ ```python
30
+ from nookara import Client, login, scope_mask
31
+ ```
32
+
33
+ `import velora_api` keeps working for older code, and the examples below still use that name.
34
+
35
+ **What's in it:** sign-in, profiles, friends, DMs, group chats, notifications, storage, credits, Ember chat and bots, bot payments and carts, Products, cart and direct pay, claims and reports, Nookara Drive, Forums, Devices and Apps, as one named method per route.
36
+
37
+ **Who can call what.** Some routes only work for a website sign-in or one of Nookara's own apps, and the server refuses anything else. Each method's entry in `catalogue.json` has a `surface`:
38
+
39
+ | surface | Who can call it |
40
+ |---|---|
41
+ | (none) | Follows the route's own auth and scopes, the normal case for your App |
42
+ | `any` | Any signed-in token: `add_to_cart`, `product_access` |
43
+ | `app` | An App with Transfer Credits: `quote_directly`, `pay_directly`. Up to 100 credits a day per App per person |
44
+ | `website` | Website sign-in only: your Products, sales, purchases, reading the cart, paying at checkout, reports and claims, managing Apps. Listed so the reference is complete, your App can't call them |
45
+ | `drive` | Nookara Drive's own app only, or Nookara for Windows |
46
+ | `forums` | Nookara Forums' own app only, for anything that changes something. Reading needs no token |
47
+ | `devices` | Nookara for Windows or a website sign-in |
48
+
49
+ To charge someone from your App, send them to checkout with `add_to_cart`, or use `pay_directly` if they gave you Transfer Credits.
50
+
51
+ ## About this client
52
+
53
+ Nookara is the identity and platform API: accounts, direct messages, groups, friends,
54
+ credits, instance tickets, signing keys and hosting. **Bonfire** is the chat system. Every
55
+ Bonfire server is an **instance** at its own web address (the main community is
56
+ `https://chat.ikelene.net`; Nookara-hosted ones are `https://<slug>-bonfire.ikelene.net`;
57
+ you can also self-host). One instance holds one community. This package talks to both.
58
+
59
+ It supports Python 3.10+ and exposes every documented REST operation as a named
60
+ method, plus a generic request method. REST needs only the Python standard
61
+ library. The optional `realtime` extra installs `websockets`; the optional
62
+ `signing` extra installs `cryptography` for message signing and verification.
63
+
64
+ ## Install
65
+
66
+ Use the commands at the top of this page. From a source checkout, run `python -m pip install ".[realtime]"`
67
+ inside the folder containing `pyproject.toml`. `requirements.txt` installs the
68
+ optional websocket dependency only, not the Nookara package itself.
69
+
70
+ ## Sign in, and what scopes mean now
71
+
72
+ ```python
73
+ from velora_api import Client, login, scope_mask
74
+
75
+ token = login(project_name="My Python client", scopes=scope_mask("PROFILE_READ", "CHAT"))
76
+ velora = Client(token)
77
+ print(velora.scope_names()) # what the person actually allowed
78
+ print(velora.bonfire_instances()) # the Bonfire servers saved on this account
79
+ ```
80
+
81
+ The browser shows a consent screen. **Scopes are requested, never required**: the person
82
+ can untick any of them (the risky ones start unticked), so a token can come back with
83
+ fewer scopes than you asked for. Ask only for what you use, then check:
84
+
85
+ ```python
86
+ velora.has_scope("MEDIA_UPLOAD") # True / False (names, labels such as "Upload Media", or bits)
87
+ velora.granted_scopes() # the usable mask (approved and not switched off by an admin)
88
+ velora.require_scope("CHAT") # raises ScopeError with a clear message
89
+ ```
90
+
91
+ A call without the scope raises `ScopeError` (a subclass of `APIError`) with `scope`,
92
+ `label` and `disabled_by_admin`, so you can show "log in again to use this" instead of
93
+ crashing. `login()` never asks for a password. Project names that start with
94
+ `Bonfire (` are reserved for the official apps. `optional_scopes=` is still accepted
95
+ and merged into `scopes`. To print the URL yourself use `login(..., on_url=print)`.
96
+ Do not print the returned token. For an existing token use `Client(os.environ["VELORA_TOKEN"])`.
97
+
98
+ | Scope | Bit | Default on the consent screen | Bonfire instance scope |
99
+ |---|---|---|---|
100
+ | View Basic Profile | 1 | on | |
101
+ | View Sensitive Info | 2 | off | |
102
+ | Project Auth | 4 | on | |
103
+ | Modify Settings | 8 | off | |
104
+ | Read Storage / Write Storage | 16 / 32 | on / off | |
105
+ | Read Instant Messages / Send Instant Messages | 64 / 128 | on | |
106
+ | View Friends / Manage Friends | 256 / 512 | on / off | |
107
+ | Spend Credits | 1024 | off | |
108
+ | Chat Messaging | 2048 | on | `chat` (needed to sign in to an instance) |
109
+ | Upload Media | 4096 | on | `media` (uploads) |
110
+ | View Subscriptions | 8192 | on | |
111
+ | Nookara Voice | 16384 | on | `voice` |
112
+ | Sign Messages | 32768 | off | `sign` (signed messages) |
113
+
114
+ `velora_api.SCOPES`, `SCOPE_LABELS`, `DEFAULT_OFF`, `INSTANCE_SCOPES`,
115
+ `scope_mask()`, `scope_names()` and `has_scope()` give you the same table in code.
116
+
117
+ ## Bonfire servers (instances)
118
+
119
+ ```python
120
+ from velora_api import Client, Instance
121
+
122
+ velora = Client(token) # needs Chat (2048)
123
+ chat = Instance("https://chat.ikelene.net")
124
+ info = chat.discover() # /.well-known/bonfire, no login
125
+ print(info["name"], info["accounts_mode"], info["build"]["version"], info["read_only"])
126
+ chat.login_with_velora(velora, sign="auto") # Nookara-login instances
127
+ # chat.login("name", "password") # local-account instances
128
+ # chat.register("name", "password")
129
+
130
+ print(chat.scopes) # instance scopes of this session: chat, voice, media, sign
131
+ channel = chat.channel_id("#general")
132
+ for message in chat.messages(channel, limit=20)["messages"]:
133
+ print(message["author"]["username"], message["body"])
134
+ chat.send_message(channel, "Hello from Python!")
135
+ ```
136
+
137
+ `login_with_velora` asks Nookara for a short ticket made for that one instance and
138
+ trades it at `POST /api/v1/auth/velora`. Your Nookara token never leaves Nookara. The
139
+ ticket carries the instance scopes the person allowed (`chat`, `voice`, `media`, `sign`);
140
+ `sign="auto"` signs only when Sign Messages was allowed, `sign=True` raises `ScopeError`
141
+ if not. The instance session lasts 30 days from last use.
142
+
143
+ Discovery helpers: `accounts_mode`, `features`, `build` (version, commit, build_id),
144
+ `read_only`, `instance_info()` (`GET /api/v1/instance/info`, admins also get runtime, stats
145
+ and `update`) and `update_available()`. `Client.server_latest()` returns the official
146
+ build instances compare with.
147
+
148
+ Useful methods: `servers()` / `joined_communities()`, `channels()`, `members()`,
149
+ `send_message()`, `edit_message()`, `delete_message()`, `react()`, `pin()`, `search()`,
150
+ `join()`, `invites()`, `create_invite(expires_in=, max_uses=)`, `delete_invite()`,
151
+ `join_invite(code)`, `invite_info(code)`, `join_requests()`, `accept_join_request(uid)`,
152
+ `create_channel()`, `set_channel_permissions()`, `kick()`, `webhooks()`,
153
+ `create_webhook()`, `bots()`, `create_bot()`, `backups()`, `create_backup()`, `settings()`,
154
+ `identities(ids)`, `set_notify()`. Every route also has a named method
155
+ (`get_api_v1_servers_by_sid`) and `instance.call(...)`. IDs are strings; users look like
156
+ `v:123`, `l:7`, `b:3`.
157
+
158
+ ### Your servers as one object
159
+
160
+ ```python
161
+ from velora_api import Bonfire
162
+
163
+ bonfire = Bonfire(velora, sign="auto")
164
+ bonfire.join_default() # the official server, once per account
165
+ for item in bonfire.communities(): # one entry per saved server
166
+ print(item["instance"], (item["community"] or {}).get("name"), item["error"])
167
+ bonfire.add("https://camp-bonfire.ikelene.net") # saved on the account, follows the person between devices
168
+ ```
169
+
170
+ A server that is down, banned, in local-account mode, or where Chat was declined is
171
+ reported in `error` (`unreachable`, `local_accounts`, `chat_scope_missing`, an API code);
172
+ it never stops the others.
173
+
174
+ ### Uploads and media import
175
+
176
+ ```python
177
+ uploaded = chat.upload("picture.png", cid=channel) # needs the media scope
178
+ saved = chat.import_media("https://example.com/cat.png", cid=channel) # the instance fetches an https URL
179
+ chat.send_message(channel, "A picture", attachments=[uploaded, saved])
180
+ ```
181
+
182
+ A session without the `media` scope raises `ScopeError` before anything is sent.
183
+ Uploads also need the community's `allow_uploads` and the channel's Attach permission.
184
+
185
+ ### Signing and verifying
186
+
187
+ ```python
188
+ chat.login_with_velora(velora, sign=True) # account key from Nookara, kept in memory
189
+ sent = chat.send_message(channel, "Signed")
190
+ ok, reason = chat.verify_message(sent["message"]) # you check it, not the instance's `verified` flag
191
+ ```
192
+
193
+ Signing is optional on every velora-mode instance and never required. It needs the
194
+ Sign Messages scope and the `signing` extra. Signatures follow protocol section 8, including
195
+ `url:` entries for link-only GIFs. `verify_message` reads the public key from Nookara and
196
+ checks owner, revocation time and the Ed25519 signature.
197
+
198
+ ### Webhooks need no login
199
+
200
+ ```python
201
+ from velora_api import execute_webhook
202
+ execute_webhook("https://chat.ikelene.net/api/v1/webhooks/ID/TOKEN", "Build finished")
203
+ ```
204
+
205
+ ### Bots
206
+
207
+ An **instance bot** is created on one instance (`chat.create_bot("pingbot")` returns the
208
+ token once) and uses a bot token. REST is Discord-style at `/bot/v1` with numeric ids, the
209
+ bot gateway is `/bot/gateway`:
210
+
211
+ ```python
212
+ bot = Instance("https://chat.ikelene.net", bot_token=os.environ["BONFIRE_BOT_TOKEN"])
213
+ async with bot.bot_gateway() as gateway:
214
+ async for event in gateway: # {"type": "MESSAGE_CREATE", "data": {...}}
215
+ ...
216
+ bot.bot_send("NUMERIC_CHANNEL_ID", "pong")
217
+ ```
218
+
219
+ A **Nookara developer bot** lives on your Nookara account and can be listed in the bot
220
+ gallery (Discovery > Bots) so others add it to their servers. Each instance makes its own
221
+ bot account for it, but the token is the same. A bot program serves all of them:
222
+
223
+ ```python
224
+ from velora_api import BotProgram
225
+
226
+ async with BotProgram(os.environ["VELORA_BOT_TOKEN"]) as bot: # GET /bonfire/bots/self/installs
227
+ async for event in bot: # {"instance", "type", "data", "seq"}
228
+ if event["type"] == "MESSAGE_CREATE" and event["data"]["content"] == "!ping":
229
+ await bot.send(event["instance"], event["data"]["channel_id"], "pong")
230
+ ```
231
+
232
+ It opens one gateway per install, re-reads the install list every 5 minutes, and keeps
233
+ going when one instance fails. Gallery owner calls: `list_gallery()`, `save_gallery_listing()`,
234
+ `submit_to_gallery()`, `gallery_status()`, `withdraw_from_gallery()`, `gallery_ticket()` and
235
+ `Instance.add_gallery_bot(ticket)`.
236
+
237
+ ### User gateway
238
+
239
+ ```python
240
+ chat.login_with_velora(velora)
241
+ async with chat.gateway() as gateway:
242
+ async for event in gateway:
243
+ if event["type"] == "chat_message":
244
+ print(event["message"]["body"])
245
+ # or: @gateway.on("chat_message") ... then: await gateway.run()
246
+ await_ack = await gateway.send_message(channel, "hi") # waits for chat_ack
247
+ ```
248
+
249
+ It reconnects with capped backoff and jitter (1s to 30s), pings every 25 s, and stops on
250
+ close codes 4001 and 4003. There is no replay: after `gateway_ready` follows a drop,
251
+ refetch history over REST and dedupe by id.
252
+
253
+ ### Other Nookara-side Bonfire helpers
254
+
255
+ `Client`: `instance_token(url)`, `signing_key()`, `bonfire_instances()`,
256
+ `set_bonfire_instances(urls)`, `sync_bonfire_instances(urls)`, `bonfire_default_instance()`,
257
+ `bonfire_default_joined()`, `bonfire_instance_prefs()`, `bonfire_directory(q)`,
258
+ `bonfire_profiles(ids)`, `bonfire_key(kid)`, `bonfire_jwks()`, `bonfire_scopes()`,
259
+ `bonfire_import_platforms()` / `start_bonfire_import()` (Discord and Fluxer bundles),
260
+ and hosting: `hosting_plans()`, `hosting_servers()`,
261
+ `create_hosting_server(slug, name, plan, weeks=)`, `buy_hosting_weeks(slug, weeks)`,
262
+ `backup_hosting_server(slug)`, `hosting_backups()`, `upload_hosting_backup(...)`,
263
+ `delete_hosting_server(slug)`. Plus is prepaid in weeks and never renews by itself; it is
264
+ free for Nookara Pro. Examples are in `examples/bonfire_*.py`.
265
+
266
+ ## Nookara REST methods
267
+
268
+ Nookara-hosted chat servers (`/chat/servers`, `/chat/channels` and so on) are
269
+ legacy and read-only now (writes answer `server_read_only` with `moved_to`). Direct
270
+ messages, groups, friends, storage, credits and presence are unchanged.
271
+
272
+ ```python
273
+ velora.send_dm("someone", "Hello from Python!")
274
+ velora.set_activity("My game", state="In the menu", ttl=600)
275
+ velora.clear_activity()
276
+ ```
277
+
278
+ The old `servers()`, `server()`, `messages()` and `send_message()` helpers on `Client`
279
+ still call the legacy routes (they now emit a `DeprecationWarning`); use `Instance` for
280
+ new work.
281
+
282
+ Group chats have reactions, edits and pins, and DMs have pins (0.6.3):
283
+
284
+ ```python
285
+ velora.patch_groups_by_room_messages_by_id(room="grp_x", id="a1b2c3", json={"message": "fixed"}) # your own message
286
+ velora.delete_groups_by_room_messages_by_id(room="grp_x", id="a1b2c3") # your own message
287
+ velora.put_groups_by_room_messages_by_id_reactions_by_emoji(room="grp_x", id="a1b2c3", emoji="\N{THUMBS UP SIGN}")
288
+ velora.delete_groups_by_room_messages_by_id_reactions_by_emoji(room="grp_x", id="a1b2c3", emoji="\N{THUMBS UP SIGN}")
289
+ velora.put_groups_by_room_messages_by_id_pin(room="grp_x", id="a1b2c3") # any member, 50 per group
290
+ velora.get_groups_by_room_pins(room="grp_x")["messages"]
291
+ velora.put_im_by_id_pin(id=123) # either person, 50 per conversation
292
+ velora.get_im_with_by_username_pins(username="someone")["messages"]
293
+ ```
294
+
295
+ The realtime client gets `room_message_edited`, `room_message_reactions`, `room_message_deleted`, `room_pins_update`,
296
+ `im_deleted` and `im_pins_update`.
297
+
298
+ `send_dm(username, message)` requires Send Instant Messages (128). Storage uses Read (16) /
299
+ Write (32); its values are strings. Use `json.dumps()` / `json.loads()` for structured data.
300
+
301
+ Every operation in the downloadable catalogues (Nookara on `Client`, instances on `Instance`)
302
+ has a named method. For example:
303
+
304
+ ```python
305
+ chat.post_api_v1_servers_by_sid_roles(
306
+ sid="YOUR_SERVER_SID", json={"name": "Member", "permissions": 257}
307
+ )
308
+ chat.call("get_api_v1_servers_by_sid", sid="YOUR_SERVER_SID")
309
+ velora.call("get_bonfire_hosting_plans")
310
+ velora.request("GET", "/bonfire/instances")
311
+ ```
312
+
313
+ Use `query={...}` for queries, `json={...}` for JSON, or `data={...}, files={...}` for
314
+ multipart. Path parameters are URL encoded; optional parameters may be omitted. The API
315
+ checks token scopes, server/channel roles, rank and privacy separately. File values can
316
+ also be `(filename, bytes_or_file_object, mime_type)`. This client buffers multipart data
317
+ with a 64 MiB limit; server limits can be smaller.
318
+
319
+ ## Errors
320
+
321
+ ```python
322
+ from velora_api import APIError, ScopeError, TransportError
323
+
324
+ try:
325
+ chat.upload("picture.png")
326
+ except ScopeError as exc:
327
+ print("Log in again and keep this permission ticked:", exc.label or exc.error)
328
+ except APIError as exc:
329
+ print(exc.status, exc.error, exc.retry_after, exc.moved_to)
330
+ except TransportError:
331
+ print("Network error. Check whether a write completed before retrying.")
332
+ ```
333
+
334
+ The library does not retry requests automatically. Handle 429 using Retry-After or
335
+ `retry_after` when present, with backoff. Error payloads are available as `exc.payload`.
336
+ Some endpoints return empty 204, text or binary responses; use `raw=True` for bytes.
337
+ Redirects are rejected so credentials are never forwarded.
338
+
339
+ ## Realtime user client (Nookara)
340
+
341
+ ```python
342
+ import asyncio
343
+ import os
344
+ from velora_api import RealtimeClient
345
+
346
+ async def main():
347
+ async with RealtimeClient(os.environ["VELORA_TOKEN"]) as socket:
348
+ await socket.set_activity("My Python app", state="Connected")
349
+ async for event in socket:
350
+ if event.get("type") in ("im_received", "presence_update"):
351
+ print(event)
352
+
353
+ asyncio.run(main())
354
+ ```
355
+
356
+ This is Nookara's own socket: presence, direct messages, groups, calls. Entering the context
357
+ waits for `auth_success`, checks `tos_required`, starts the reader and sends `chat_hello`
358
+ when the Chat scope is granted. Its `chat_*` frames only concern legacy Nookara-hosted
359
+ servers; Bonfire chat uses the instance gateway above. `rpc()` is only for frames with nonce
360
+ replies. On disconnect, reconnect, authenticate, refetch REST state and rebuild presence.
361
+ There is no durable websocket replay.
362
+
363
+ This package transports voice signaling but does not implement WebRTC, audio
364
+ capture/playback or a desktop UI. See the voice guide for a WebRTC implementation,
365
+ including two peer connections and subscription snapshots.
366
+
367
+ ## Legacy Nookara bots and webhooks
368
+
369
+ These Nookara-hosted routes are read-only now; use the instance bots and webhooks above.
370
+
371
+ ```python
372
+ bot = Client(os.environ["VELORA_BOT_TOKEN"], auth_scheme="Bot")
373
+ me = bot.get_chat_api_v1_users_me()
374
+ ```
375
+
376
+ ## Game servers (Bonfire Link, Minecraft)
377
+
378
+ On a Bonfire instance the game plugin connects out to the instance
379
+ (`bonfire server https://chat.example` in the game console); an admin picks it from the
380
+ connected list:
381
+
382
+ ```python
383
+ for server in chat.game_connected()["instances"]: # waiting to be linked (Manage server)
384
+ print(server["iid"], server["pairing_tag"], server["motd"])
385
+ chat.game_link(iid, cid=GAME_CHANNEL_CID)
386
+ print(chat.game_status(GAME_CHANNEL_CID))
387
+ ```
388
+
389
+ `Client.game_*` still exist for the legacy Nookara routes. Console commands may require the
390
+ website or the Bonfire app (`console_needs_bonfire`).
391
+
392
+ ## Bot payments (0.5.0)
393
+
394
+ A bot can ask a Nookara user for credits. The bot authenticates with its **bot token**; the buyer
395
+ opens the returned `pay_url`, signs in with their normal login (no scopes, no app authorization)
396
+ and presses Pay. The credits go to the bot's owner. There is no webhook, so you poll.
397
+
398
+ ```python
399
+ from velora_api import BotClient, is_paid, OrderConflict, BuyerError, PaymentError
400
+
401
+ bot = BotClient(os.environ["BOT_TOKEN"])
402
+ request = bot.create_payment_request("alice", 20, "ord-1001", description="Premium pack",
403
+ return_url="https://shop.example/done", expires_in=900)
404
+ print(request["pay_url"]) # send the buyer here, do not log it publicly
405
+ final = bot.wait_for_payment(request["id"], timeout=900, poll=3) # PaymentTimeout when still pending
406
+ if is_paid(final) and final["amount"] == 20: # fulfil ONLY when paid; check amount and buyer id
407
+ ...
408
+ ```
409
+
410
+ `get_payment_request(id)`, `find_payment_request(order_id)` (None if unknown),
411
+ `list_payment_requests(status=None, limit=None)` and `cancel_payment_request(id)` complete the set.
412
+ Repeating `create_payment_request` with the same order id, buyer and amount returns the existing request
413
+ (`idempotent_replay` is true); a different buyer or amount raises `OrderConflict`. Errors are
414
+ `PaymentError` subclasses (still `APIError`): `InvalidBotToken`, `MerchantUnavailable`,
415
+ `InvalidPaymentRequest`, `BuyerError`, `OrderConflict`, `PaymentInProgress`, `NotCancellable`,
416
+ `PaymentNotFound` and `PaymentRateLimited`; `error.error` keeps the server code. Run this on a server:
417
+ **never put a bot token in a Scratch/PenguinMod project, web page or app** (the PenguinMod extension has no
418
+ payment blocks for that reason). See `examples/bot_payments.py` and the Bot payments section of the guide.
419
+
420
+ Bot presence: a bot is online in a community while it holds an authenticated `/bot/gateway` connection.
421
+ Member objects from `Instance.members(...)` carry `presence` (`"online"` or `"offline"` for bots, `null` for
422
+ people) and the instance websocket (`InstanceGateway`) delivers `chat_presence` events; the library passes these through unchanged.
423
+ `Client.bot_installs()` includes staff test installs with `test: true` (not counted as real installs; skip them).
424
+
425
+ ## Notifications (0.5.7)
426
+
427
+ Friend requests, credits, store sales, badges, membership, hosting and messages from the Nookara team are
428
+ notifications (the bell on the site and in Ember). System DMs are gone: those messages are notifications now.
429
+
430
+ ```python
431
+ page = velora.notifications(unread=True, limit=20) # {"notifications": [...], "next_cursor": ..., "unread": n}
432
+ for n in page["notifications"]:
433
+ print(n["category"], n["title"], n["body"]) # plain text, **bold** is the only markup
434
+ velora.mark_notifications_read([n["id"] for n in page["notifications"]]) # or mark_notifications_read(all=True)
435
+ print(velora.unread_notifications())
436
+ for category in velora.notification_settings(): # locked ones (system, moderation) are always on
437
+ print(category["key"], category["list"], category["email"], category["locked"])
438
+ velora.set_notification_settings({"friends": {"email": False}})
439
+ ```
440
+
441
+ `mark_notifications_unread(ids)` and `delete_notification(id)` complete the set; the generated methods are
442
+ `get_account_notifications`, `get_account_notifications_unread_count`, `post_account_notifications_read`,
443
+ `post_account_notifications_unread`, `delete_account_notifications_by_id`, `get_account_notification_settings`
444
+ and `patch_account_notification_settings`. Live changes arrive on the user websocket:
445
+
446
+ ```python
447
+ async with RealtimeClient(os.environ["VELORA_TOKEN"]) as socket:
448
+ async for event in socket.notification_events(): # notification_new / notifications_updated
449
+ print(event["type"], event["unread"])
450
+ ```
451
+
452
+ `notification_new` carries `notification` (same shape as the list) and `unread`; `notifications_updated` carries
453
+ `unread` and one of `read` (ids or `"all"`), `unread_ids` or `deleted`. The older `/notifications` routes still
454
+ exist for the Scratch extension. Staff can send with `post_admin_notifications_send`.
455
+
456
+ ## Bot aliases (0.5.8)
457
+
458
+ A bot can post a message under another name and avatar. The message still belongs to the bot (mentions,
459
+ moderation and reports use the real bot); clients show the alias with a mask icon and the tooltip
460
+ "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`.
461
+
462
+ ```python
463
+ from velora_api import Instance, make_alias
464
+
465
+ bot = Instance("https://chat.ikelene.net", bot_token=os.environ["BOT_TOKEN"])
466
+ sent = bot.bot_send("CHANNEL_ID", "Welcome in!", alias={"name": "Narrator", "avatar_url": "https://example.com/n.png"})
467
+ print(sent["alias"]) # {"name": "Narrator", "avatar_url": "https://example.com/n.png"}
468
+ bot.bot_edit("CHANNEL_ID", sent["id"], alias=make_alias("Storyteller")) # change it
469
+ bot.bot_edit("CHANNEL_ID", sent["id"], alias=None) # clear it
470
+ ```
471
+
472
+ `name` is 1-32 characters with no line breaks and `avatar_url` is optional (`None` or `""` = no avatar), `https://` only, at most 500
473
+ 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
474
+ `BotProgram.send(...)` (through its fields), `Instance.send_message`, `Instance.edit_message` and
475
+ `InstanceGateway.send_message`. Messages are plain dicts in this library, so read `message["alias"]`, which is
476
+ `None` or `{"name", "avatar_url", "unmarked"}`; `message["author"]` is always the real bot.
477
+
478
+ Unmarked aliases: when an Ember server's owner turns on Server settings > Danger zone > Advanced > "Allow unmarked aliases"
479
+ (`advanced.unmarked_aliases`, owner only) and the bot has the "Use unmarked aliases" role permission (4096) or Administrator,
480
+ its aliases come back with `"unmarked": True` and, when the name matched exactly one member of that server, `"member_id"`
481
+ (a principal on the client API, a numeric id on the bot API). Clients then show the alias like a normal member message. Nothing
482
+ changes in this library: the fields are just more dict keys, and `message["author"]` is still the real bot.
483
+
484
+ ## Complete reference
485
+
486
+ https://nookara.cc/guides/velora-api
487
+
488
+ The source catalogue records the live API on 2026-10-19, including staff routes. Not every
489
+ input has a strict JSON Schema and not every privileged operation is tested live. The
490
+ server remains authoritative. Internal bridge/payment integration credentials and
491
+ deployment callbacks are not public client APIs.
492
+
493
+ ## Changes in 0.6.1
494
+
495
+ - Fix `bad_signature` when sending signed messages after the move to Nookara: `enable_signing()` now signs with the issuer the instance announces (`/.well-known/bonfire` `velora.issuer`, falling back to `https://velora.ikelene.net`) instead of the base URL you logged in through. `Instance.issuer()` returns it, and `verify_message()` accepts the announced issuer first, then the other known Nookara origins.
496
+
497
+ ## Changes in 0.6.0
498
+
499
+ - Velora is now Nookara: the default base URL is `https://nookara.cc` (and `wss://nookara.cc/` for realtime). `velora.ikelene.net` keeps working for now; pass `base_url=` / `url=` to keep using it. The import name `velora_api` is unchanged, and a thin `nookara` package re-exports it (`import nookara`).
500
+
501
+ ## Changes in 0.5.10
502
+
503
+ - DM files no longer go to catbox.moe. `upload_dm_file(path, destination="temp" | "account")` uploads to Nookara's
504
+ temporary storage (`POST /chat/media/temp`, links at `https://nookara.cc/t/<id>/<name>`, the expiry resets
505
+ on every load) or to your account storage (`POST /chat/media/account`). `upload_destinations()` /
506
+ `get_chat_media_destinations()` says what's available and how much account space is left.
507
+ `post_chat_media_external()` still works and now uploads to temporary storage too.
508
+
509
+ ## Changes in 0.5.9
510
+
511
+ - Realtime: Nookara sends `user_update {user, changed, previous_username?, v}` on the user socket whenever someone you can see
512
+ changes their profile (picture, banner, colour, pronouns, bio, website, country, username). It arrives through
513
+ `async for event in socket` like any other frame; patch cached copies by `user["id"]`. No API change.
514
+
515
+ ## Changes in 0.5.8
516
+
517
+ - Bot aliases: `alias=` on `Instance.bot_send`, `Instance.bot_edit` (`alias=None` clears), `Instance.send_message`,
518
+ `Instance.edit_message` and `InstanceGateway.send_message`, plus `make_alias(name, avatar_url=None)` which checks
519
+ the limits. `bot_edit` now takes `content` as optional so an alias can be changed alone. Nothing else changed.
520
+
521
+ ## Changes in 0.5.7
522
+
523
+ - Notifications: new generated methods for `/account/notifications` (list, unread count, read, unread, delete),
524
+ `/account/notification-settings` (GET, PATCH) and the staff `POST /admin/notifications/send`, helpers
525
+ `notifications()`, `unread_notifications()`, `mark_notifications_read()`, `mark_notifications_unread()`,
526
+ `delete_notification()`, `notification_settings()`, `set_notification_settings()`, and
527
+ `RealtimeClient.notification_events()` with `NOTIFICATION_EVENT_TYPES` (`notification_new`, `notifications_updated`).
528
+ - System DMs (`from_system` messages) are no longer created; those messages are notifications now.
529
+
530
+ ## Changes in 0.5.1
531
+
532
+ - New `get_chat_media_limits()`: the real upload limits (`server_mb` for files sent in a conversation, `saved_mb` for files kept
533
+ on your Nookara account). `get_chat_unfurl()` can now return `payment` cards for Nookara payment links. Nothing else changed.
534
+
535
+ ## Changes in 0.5.0
536
+
537
+ - New `BotClient` (bot token) with bot payment helpers: `create_payment_request`,
538
+ `get_payment_request`, `find_payment_request`, `list_payment_requests`, `cancel_payment_request`,
539
+ `wait_for_payment`, `is_paid`, plus the `PaymentError` family. The four `/bot/payment-requests`
540
+ routes are also generated methods (`post_bot_payment_requests`, ...).
541
+ - Docs for bot presence and staff test installs (`test: true`). No behaviour changes elsewhere.
542
+
543
+ ## Changes in 0.4.0
544
+
545
+ - Scopes are only requested: `login(scopes=...)` sends them as an optional request, the new
546
+ `scopes` module and `Client.granted_scopes()/has_scope()/require_scope()` report what the
547
+ person allowed, and calls without a scope raise `ScopeError` (an `APIError` subclass).
548
+ - Instance scopes (`chat`, `voice`, `media`, `sign`) from the ticket: `Instance.scopes`,
549
+ `has_scope()`, `sign="auto"`, early `ScopeError` for uploads.
550
+ - New: `Bonfire` (your saved servers), `BotProgram` (a bot on every instance it is
551
+ installed on), `verify_message`, invites, join requests, media import, channel and role
552
+ helpers, game/bridge/admin helpers, instance info and build, Nookara helpers for profiles,
553
+ keys, JWKS, scopes, import jobs, default server and push preferences.
554
+ - 458 Nookara and 169 instance operations as named methods.
555
+ - Legacy `Client.servers()/server()/messages()/send_message()` warn that Nookara-hosted chat is read-only.
556
+
557
+ ## Encrypted direct messages
558
+
559
+ DMs are end-to-end encrypted, so `Client.send_dm` no longer sends plain text. A client makes its own keys and asks the person to approve it in Ember:
560
+
561
+ ```python
562
+ from velora_api import EncryptedDMs
563
+
564
+ dms = EncryptedDMs(client, key_file="nookara-dms.json") # pip install "nookara[signing]"
565
+ if not dms.linked():
566
+ code = dms.request_link()
567
+ print("In Ember: Settings > Privacy > Approve a device, and enter", code)
568
+ dms.wait_for_approval(code)
569
+ dms.send("alice", "Hello!")
570
+ for m in dms.messages("alice"):
571
+ print(m["from"], dms.open(m))
572
+ ```
573
+
574
+ Before linking, the Direct Messages permission only gets locked messages and who/when. `key_file` holds your private keys: protect it like a password. People can unlink your client any time. Someone aged 13-15 can't give this permission to other people's Apps.