Usage

Handling events

Register handlers with options. Specific rules first; a catch-all last. Python uses a decorator or on_message(opts, fn). TypeScript chains onMessage(opts, fn).

on_message

@cx.on_message({
    "channel": ["discord", "telegram"],  # one name or a list
    "kind": "dm",                        # dm / group / channel
    "command": ["start", "help"],        # first token; /help@Bot works
    "overlap": "queue",                  # queue | debounce | drop | parallel | stream
    "bound": 16,
    "ack": "On it…",                 # acknowledgement: instant reply before the handler
})
def handle(thread, msg, ctx):
    # ctx.skipped — events collapsed before this turn, not ones you ignored
    thread.post(tutor(msg, ctx.skipped))
cx.onMessage(
  {
    channel: ["discord", "telegram"],  // one name or a list
    kind: "dm",                        // dm / group / channel
    command: ["start", "help"],        // first token; /help@Bot works
    overlap: "queue",                  // queue | debounce | drop | parallel
    bound: 16,
    ack: "On it…",                     // acknowledgement: instant reply before the handler
  },
  async (thread, msg, { skipped }) => {
    // skipped — events collapsed before this turn, not ones you ignored
    await thread.post(await tutor.run(msg, skipped))
  },
)

kind is the chat kind. The method name already means “message events.”

overlap is what happens when another event arrives for the same key while this handler is still running. Defaults and policies are on Overlap.

ack is an acknowledgement: a user-visible reply posted immediately, before your handler runs. Use it on email, SMS, and X (no typing indicator). Button-press acknowledgement (stopping the spinner) is separate — the adapter always does that.

on_action

Button presses. The adapter always acknowledges so a spinner cannot hang because the author forgot. Prefer overlap: "drop" so a tap never waits behind a paragraph — if a handler is already running for that key, this press is skipped. See Overlap.

from caspian import Action, Button

@cx.on_message({"channel": "telegram", "command": "buttons"})
def on_buttons(thread, msg, ctx):
    thread.post("pick one:", actions=(
        Button(label="snooze", data="snooze"),
        Button(label="done", data="done"),
    ))

@cx.on_action({"channel": "telegram", "data": "done", "overlap": "drop"})
def on_done(thread, action: Action, ctx):
    thread.edit(action.message_id, "done.")

@cx.on_action({"channel": "telegram"})
def on_other(thread, action, ctx):
    thread.post(f"you picked {action.data}")
cx.onMessage({ channel: "telegram", command: "buttons" }, async (thread) => {
  await thread.post("pick one:", {
    actions: [
      { label: "snooze", data: "snooze" },
      { label: "done", data: "done" },
    ],
  })
})

cx.onAction({ channel: "telegram", data: "done", overlap: "drop" }, async (thread, action) => {
  await thread.edit(action.message_id, "done.")
})

cx.onAction({ channel: "telegram" }, async (thread, action) => {
  await thread.post(`you picked ${action.data}`)
})

Order matters

More specific predicates first. A final on_message({ channel: "telegram" }) is the echo. Linear webhooks also fire for comments the bot just posted — do not echo, or you loop.

Testing without a network

interp = cx.interpret()   # MemoryInterpreter
# feed events, assert commands — no token, no ngrok
const interp = await cx.interpret()
// feed events, assert commands — no token, no ngrok