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 = client.create_dm(username="alice")
client.send_message(dm_channel.id, "Hey Alice!")

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

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')"))

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.0.tar.gz (9.9 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.0-py3-none-any.whl (10.7 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: holt-0.1.0.tar.gz
  • Upload date:
  • Size: 9.9 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.0.tar.gz
Algorithm Hash digest
SHA256 da58e2a2059573326ca4ec0375c072d6d77d3921d395c9a077aa1a22f8e150fc
MD5 1c0382726f090be2e4fd7193ea29c1b2
BLAKE2b-256 fd472ecf1727f93020b41883b52e38ae571040f303526abb8fbc18fa520f42af

See more details on using hashes here.

File details

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

File metadata

  • Download URL: holt-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 10.7 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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0b40a31fdfc1053e6fe509c3b9d5b7e64c2031e7b335cac4ea30771e803bd0c9
MD5 9b6162a4a0829bffae4658863c7cc0ee
BLAKE2b-256 548510216edc17d32e3f52c5a6157333b2abb589798bbed275e9496fbb7fcf90

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