Skip to main content

Python client SDK for the Holt chat app bot API with end-to-end encryption

Project description

Holt Chat logo

Holt Python SDK

Python client SDK for the Holt chat app bot API, with full end-to-end encryption.

License: MIT PyPI Python


Installation

pip install holt
# or from source:
pip install -e /path/to/holt-sdk/python

Quickstart

Send a message

from holt import HoltClient

# Your bot token (30-char string) and RSA-2048 private key as base64(PKCS8 DER)
BOT_TOKEN = "your-30-char-bot-token-here"
PRIVATE_KEY_B64 = "your-base64-pkcs8-der-private-key"

client = HoltClient(
    base_url="https://your-holt-instance.example.com",
    token=BOT_TOKEN,
    private_key_b64=PRIVATE_KEY_B64,
)

# Get bot info
me = client.get_me()
print("Logged in as:", me["username"])

# List channels
channels = client.get_channels()
for ch in channels:
    print(ch.id, ch.type, ch.name)

# Send a message (auto-detects channel type, handles E2EE)
result = client.send_message(channel_id="your-channel-id", text="Hello from the bot!")
print("Sent:", result.message_id)

# Get recent messages (auto-decrypts E2EE content)
messages = client.get_messages(channel_id="your-channel-id", limit=20)
for msg in messages:
    if msg.decrypt_error:
        print("[undecryptable]", msg.decrypt_error)
    else:
        print(f"{msg.user.username if msg.user else 'unknown'}: {msg.content}")

client.close()

Listen for real-time messages and echo-reply

from holt import HoltClient

BOT_TOKEN = "your-30-char-bot-token-here"
PRIVATE_KEY_B64 = "your-base64-pkcs8-der-private-key"

client = HoltClient(
    base_url="https://your-holt-instance.example.com",
    token=BOT_TOKEN,
    private_key_b64=PRIVATE_KEY_B64,
)

me = client.get_me()

@client.on("message_sent")
def on_message(payload):
    msg = payload["message"]
    channel_id = payload["channel_id"]
    # Ignore messages from the bot itself
    if msg.user and msg.user.username==me["username"]:
        return
    if msg.decrypt_error:
        print(f"[{channel_id}] Could not decrypt message from {msg.user.username if msg.user else '?'}")
        return
    print(f"[{channel_id}] {msg.user.username if msg.user else '?'}: {msg.content}")
    # Echo the message back
    client.send_message(channel_id, f"Echo: {msg.content}", reply_to=msg.id)

@client.on("typing")
def on_typing(payload):
    print(f"{payload.get('username')} is typing in {payload.get('channel_id')}")

print("Listening for events... (Ctrl-C to stop)")
client.run()  # blocks; connects SSE stream and dispatches events

Create a DM / join via invite

dm_channel_id = client.create_dm(username="alice")
client.send_message(dm_channel_id, "Hey Alice!")

channel_id = client.join_invite("invite-code-here")
print("Joined:", channel_id)

Error handling

from holt import HoltClient, HoltError

try:
    client.send_message("bad-channel", "hello")
except HoltError as e:
    print(f"HTTP {e.status}: {e.message}")
    if e.rate_limit_reset:
        print(f"Rate limited, reset at epoch: {e.rate_limit_reset}")
    print("Full body:", e.body)

Buttons and modals

from holt import button, action_row, text_input, BUTTON_STYLE_SUCCESS, BUTTON_STYLE_DANGER

client.send_message(channel_id, "Pick an option:", components=[
    action_row(
        button("Approve", custom_id="approve", style=BUTTON_STYLE_SUCCESS),
        button("Deny", custom_id="deny", style=BUTTON_STYLE_DANGER),
    )
])

@client.on("component_interaction")
def on_click(payload):
    if payload["custom_id"]=="deny":
        client.respond_with_modal(
            payload["interaction_id"],
            title="Denial reason",
            custom_id="deny_modal",
            components=[action_row(text_input("reason", "Why are you denying this?", style=2, max_length=500))],
        )
    else:
        client.ack_interaction(payload["interaction_id"])

@client.on("modal_submit")
def on_modal(payload):
    print("Reason:", payload["components"]["reason"])

Edit an existing message's buttons with client.edit_message(channel_id, message_id, components=[...]); pass components=[] to clear them, or omit the parameter to leave them unchanged.

Slash commands

client.set_commands([
    {
        "name": "announce",
        "description": "Post an announcement",
        "options": [
            {"name": "message", "description": "Text to post", "type": "string", "required": True, "max_length": 500}
        ],
    }
])

@client.on("interaction_create")
def on_command(payload):
    if payload["command"]=="announce":
        client.send_message(payload["channel_id"], payload["options"]["message"])

set_commands atomically replaces the bot's entire command set. Use upsert_command/delete_command to manage commands one at a time.

Rich content blocks

Diagrams and sandboxed interactive widgets are just markers inside a message's text, built with helpers and appended to the content you send:

from holt import diagram_block, interactive_block

client.send_message(channel_id, "Here's the flow:\n" + diagram_block("graph TD; A-->B;"))
client.send_message(channel_id, "Try it:\n" + interactive_block(html="<button>Click</button>", js="document.querySelector('button').onclick=()=>alert('hi')"))

Embeds

embed_block(**fields) covers title, description, color, url, author {name, url}, footer {text}, fields [{name, value, inline}], and timestamp. Images and thumbnails aren't plain URLs though — the server stores them as uploaded assets, so upload first and reference the returned asset id:

from holt import embed_block

with open("chart.png", "rb") as f:
    asset = client.upload_embed_asset(channel_id, f.read(), filename="chart.png", content_type="image/png")

client.send_message(channel_id, embed_block(
    title="Weekly report",
    description="Traffic is up 12% week over week.",
    color=0x6f42c1,
    image={"id": asset["id"]},
))

Pass encrypt=True to upload_embed_asset when posting in a DM or encrypted group channel (broadcast channels are always plaintext, so this is ignored there).

Crypto overview

  • AES-GCM-256: messages in DM and encrypted group channels are encrypted with a shared per-channel AES key.
  • RSA-OAEP: each member's copy of the AES key is wrapped with their RSA-2048 public key (OAEP with SHA-256 and label parley).
  • RSA-PSS: every message is signed by the sender's private key so recipients can verify authenticity.
  • The SDK handles all of this automatically inside send_message, edit_message, and get_messages.

Channel types

Value Constant Description
1 CHANNEL_DM Direct message (E2EE)
2 CHANNEL_ENCRYPTED_GROUP Encrypted group (E2EE)
3 CHANNEL_BROADCAST Broadcast (plaintext)

License

MIT

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

holt-0.1.1.tar.gz (10.5 kB view details)

Uploaded Source

Built Distribution

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

holt-0.1.1-py3-none-any.whl (11.4 kB view details)

Uploaded Python 3

File details

Details for the file holt-0.1.1.tar.gz.

File metadata

  • Download URL: holt-0.1.1.tar.gz
  • Upload date:
  • Size: 10.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for holt-0.1.1.tar.gz
Algorithm Hash digest
SHA256 f2d745129933dd6ccc7c450c0753cf4817ba418c022c895d13a4fdf5f8f10023
MD5 727a896112d043ad45cd0f1205ba22ce
BLAKE2b-256 b4e42592d1674abebec216ef71adfa454590f92a749d7e89214ed6fa771fa554

See more details on using hashes here.

File details

Details for the file holt-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: holt-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 11.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for holt-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 73048fd96f803373cec7f350bc4a4f06b4e172d0f08cea2a2e68f003a4c84b66
MD5 cad833f98527a3a5f446ee0e55984e43
BLAKE2b-256 8d39800e7e1a3856367bedae96391fe6d0609f8b216d0d154464e44308825d42

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