Skip to main content

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 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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

vinrog-0.1.0.tar.gz (56.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vinrog-0.1.0-py3-none-any.whl (40.4 kB view details)

Uploaded Python 3

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

Hashes for vinrog-0.1.0.tar.gz
Algorithm Hash digest
SHA256 9c2cdde1cf517ec806be000bc5c1f17ef7733ca66b176af0dac0fe74eb1d009e
MD5 8b926fa4d8bf5a7cdb49aaa368c02d7d
BLAKE2b-256 9168b1d04c7296305910b1994581e2a49aced1dc566c315b41c00cae3e7f319f

See more details on using hashes here.

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

Hashes for vinrog-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3c76bccc2bf36dec1954af036f4a6ed26a9188ec3701c254dff097412b9ef9fa
MD5 3a94bc64015a6704a3a6029d873de3ae
BLAKE2b-256 73ad4c3a57a9698adcb00931abece9b12b95af50bd71e1cf237c5ebc41fdb964

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page