Skip to main content

vinrog

Official Python SDK for Vinrog bots: gateway, REST, and interactions.

Voice is included: await client.join_voice(channel_id) connects and publish_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_id is None on on_message_update. MESSAGE_UPDATE publishes the message flat and Message has no server_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 no Retry-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_seq rather 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.

Metadata

Release files for vinrog 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for vinrog 0.1.0
File Size Uploaded Provenance Actions
vinrog-0.1.0.tar.gz 56.3 kB Without provenance Details for vinrog-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for vinrog 0.1.0
File Interpreter ABI Platform Actions
vinrog-0.1.0-py3-none-any.whl Python 3 none any Details for vinrog-0.1.0-py3-none-any.whl

Total release size: 96.7 kB

Release files / vinrog-0.1.0.tar.gz

Download URL vinrog-0.1.0.tar.gz
Size 56.3 kB
Tags Source
SHA-256 checksum
How to use checksums
9c2cdde1cf517ec806be000bc5c1f17ef7733ca66b176af0dac0fe74eb1d009e
BLAKE2b-256 checksum
How to use checksums
9168b1d04c7296305910b1994581e2a49aced1dc566c315b41c00cae3e7f319f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.10

Release files / vinrog-0.1.0-py3-none-any.whl

Download URL vinrog-0.1.0-py3-none-any.whl
Size 40.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3c76bccc2bf36dec1954af036f4a6ed26a9188ec3701c254dff097412b9ef9fa
BLAKE2b-256 checksum
How to use checksums
73ad4c3a57a9698adcb00931abece9b12b95af50bd71e1cf237c5ebc41fdb964
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.10

Security for vinrog 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page