Official Python SDK for Vinrog bots (gateway, REST, interactions).
Project description
vinrog
Official Python SDK for Vinrog bots: gateway, REST, and interactions.
Voice is included:
await client.join_voice(channel_id)connects andpublish_pcm()streams audio. LiveKit is an optional extra, so a text-only bot never installs it.
This package implements the protocol; it does not define it. The contract lives in
protocol/ and is shared with
the Rust server and the TypeScript SDK. If this README and protocol/gateway.md ever disagree,
protocol/ wins.
Install
pip install vinrog
Usage
import asyncio
import os
from vinrog import Client
client = Client(
token=os.environ["BOT_TOKEN"],
application_id=os.environ["APP_ID"],
api_url="https://vinrog.example",
intents=["GUILD_MESSAGES"],
)
@client.event
async def on_ready(ready):
print(f"online as {ready.user_id}")
@client.event
async def on_message_create(message):
if message.author_id == client.user:
return # ignore our own messages
if message.content == "!ping":
await message.reply("pong")
asyncio.run(client.login())
Typed events, structures, builders
Every event the server dispatches is typed, and what arrives is an object with methods:
from vinrog import ActionRowBuilder, ButtonBuilder, EmbedBuilder
@client.event
async def on_message_create(message):
await message.react("👍")
sent = await message.reply(
embeds=[EmbedBuilder().set_title("Now playing").set_color("#5865f2")],
components=[
ActionRowBuilder().add_components(
ButtonBuilder().set_style("primary").set_label("Skip").set_custom_id("skip")
)
],
)
await sent.pin()
@client.event
async def on_member_join(member):
await member.add_role(role_id)
@client.event
async def on_member_leave(leave):
if leave.reason == "ban":
audit(leave)
Names stay snake_case — idiomatic in Python and identical to the wire, so there is nothing to
convert. (The TypeScript SDK uses camelCase; that is the one place the two SDKs differ.)
Builders emit Vinrog's shape, not Discord's: components are tagged with the string
"action_row" / "button", and styles are "primary" / "secondary" — not integers.
Two things the SDK deliberately does not do, because the server cannot support them:
message.server_idisNoneonon_message_update.MESSAGE_UPDATEpublishes the message flat andMessagehas noserver_id, so it is not on the wire — and with no cache there is nothing to synthesise it from. Tracked as a protocol gap, not papered over.- No per-route rate-limit buckets. The server sends no
X-RateLimit-*headers and noRetry-After, so a client cannot model a budget it cannot observe. Instead the REST client keeps one request per route in flight (serialize_by_route, on by default), which bounds your own burst. That is politeness, and it is named as such.
Every structure method maps to a route in the Vinrog server that a bot token can call, checked
against server/src/routes/. Structures are parsed from the canonical fixtures the server's own
tests deserialize, so a field rename fails a test rather than shipping.
await client.login() returns when the gateway is READY, and raises only on a fatal error — a
bad token, an unsupported protocol version, an invalid IDENTIFY. Transient failures are the SDK's
problem, not yours: it reconnects and resumes.
What the SDK does for you
Reconnection is not a loop around connect() — it is a state machine, and most of it exists to
avoid losing events silently:
- Resume with the contiguous sequence, not the highest seen. If event 11 arrives before a replayed 10, acking 11 would drop 10 permanently with no error anywhere.
- Bounded reordering during replay, closed by
replay_through_seqrather than a frame count (replayed and live frames are indistinguishable on the wire). - Zombie detection from the age of the oldest unacknowledged heartbeat — receiving other events does not prove your side of the socket is alive.
max(server hint, exponential backoff + jitter)on rate limits, so many bots limited at once don't reconnect as one synchronised wave.
Each is a rule in
protocol/gateway.md, verified
by the shared conformance suite — the same 15 scenarios the TypeScript SDK passes, run against
this package in CI.
Interactions
on_interaction_create receives a parsed dataclass, discriminated on type:
from vinrog import CommandInteraction
@client.event
async def on_interaction_create(interaction):
if isinstance(interaction, CommandInteraction):
await client.rest.reply_to_interaction(
interaction.id, interaction.token, {"content": "pong"}, ephemeral=True
)
The server sends four types — command, component, modal_submit, autocomplete. Handle the
ones you register for and ignore the rest. Note values is a list on a component (select-menu
picks) but a dict on a modal submit (field → value), which is why they are separate classes
rather than one.
Voice
voice = await client.join_voice(voice_channel_id)
async with voice:
for frame in pcm_frames: # s16le, 48 kHz stereo
await voice.publish_pcm(frame)
Needs the optional extra:
pip install "vinrog[voice]"
Without it, join_voice raises VoiceDependencyError naming the extra and the install command —
a text-only bot never loads the native wheel, because the import is lazy.
The connection is publish-only. The server mints bots a token with
can_subscribe = False, so there is no inbound audio API here — the token forbids it.
There is no voice_join(). POST /channels/{id}/voice/join enforces user_limit and writes
voice_channel_members itself, but the bot's token is minted capacity-bypassed — calling it
would consume a slot the token was designed not to consume. Connecting to LiveKit is the join.
The rest of the voice surface is REST: list_voice_members, list_active_voice,
set_self_voice_state, and moderation (move_voice_member, disconnect_voice_member,
set_server_mute, set_server_deafen).
Errors
VinrogHTTPError carries status, code and body. code is None when the response had no
envelope — normal, not exceptional: the per-IP rate limiter (429), request timeouts (408),
body-size caps (413) and unmatched routes (404) all return an empty body. Switch on status when
code is absent.
GatewayError carries .code — the close code, or the HTTP status for a handshake rejection.
ProtocolError is a subclass raised when the server sends something the contract forbids; the SDK
never papers over a missing REQUIRED field with a default.
Typing
Fully annotated and PEP 561 compliant (py.typed), so mypy and pyright see the types.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vinrog-0.1.0.tar.gz.
File metadata
- Download URL: vinrog-0.1.0.tar.gz
- Upload date:
- Size: 56.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9c2cdde1cf517ec806be000bc5c1f17ef7733ca66b176af0dac0fe74eb1d009e
|
|
| MD5 |
8b926fa4d8bf5a7cdb49aaa368c02d7d
|
|
| BLAKE2b-256 |
9168b1d04c7296305910b1994581e2a49aced1dc566c315b41c00cae3e7f319f
|
File details
Details for the file vinrog-0.1.0-py3-none-any.whl.
File metadata
- Download URL: vinrog-0.1.0-py3-none-any.whl
- Upload date:
- Size: 40.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c76bccc2bf36dec1954af036f4a6ed26a9188ec3701c254dff097412b9ef9fa
|
|
| MD5 |
3a94bc64015a6704a3a6029d873de3ae
|
|
| BLAKE2b-256 |
73ad4c3a57a9698adcb00931abece9b12b95af50bd71e1cf237c5ebc41fdb964
|