Cards
Chattice provides typed facade builders over the official google-apps-card SDK — no manual Cards JSON.
Facades
card = Card(
header=CardHeader(title="Deploy production?"),
sections=[
Section(
widgets=[
TextParagraph("Deploy v2.1?"),
ButtonList(
buttons=[
Button(
"Deploy",
action="deploy.confirm",
parameters={"env": "prod"},
),
Button("Cancel", action="deploy.cancel"),
]
),
]
)
],
)
Each facade builds the SDK proto via to_proto(), serializes via
to_dict() (documented camelCase Cards v2 JSON), and rebuilds via
from_dict()/from_proto(). Card.from_dict() takes a lossless raw JSON
snapshot: unknown top-level/known-widget fields survive to_dict(), and
unsupported widgets become RawWidget facade entries instead of raising or
being dropped. The raw .proto object remains the escape hatch for future
Google SDK fields; schema-unknown JSON cannot be retained by protobuf itself,
so use the JSON path when exact unknown-field round-trip matters.
Image(image_url=..., alt_text=..., on_click=...) and Image.from_url() are
the typed HTTPS picture widget. Image.from_path() and Image.from_bytes()
store a private immutable source description; the Bot resolves that source
through an application-provided AssetPublisher, builds a new resolved Card,
and only then serializes it. The user's Card remains unchanged. Card asset
publication is deliberately separate from message attachments and their USER
authentication semantics (see files-media).
Published Card image URLs are not a confidential-media channel: Google's image
fetcher must be able to retrieve them. Confidential generated files belong in
native USER-authenticated attachments or private Drive links.
Buttons and actions
Button(action=..., parameters=...) produces onClick.action with string
parameters — the same shape normalized into ActionEvent, so clicks
route through @router.action("deploy.confirm").
A button can also open a dialog: Button(..., interaction=ButtonInteraction.OPEN_DIALOG)
serializes onClick.action.interaction: "OPEN_DIALOG" — see
Dialogs & App Home.
Forms and validation
Form widgets carry the full documented field set:
from chattice.cards import (
DateTimePicker,
SelectionInput,
TextInput,
TextInputType,
Validation,
)
text = TextInput(
name="email",
label="Email",
hint_text="email address",
value="",
validation=Validation(character_limit=254, input_type=TextInputType.EMAIL),
)
select = SelectionInput(
name="tier",
label="Tier",
items=[{"value": "free", "text": "Free"}, {"value": "pro", "text": "Pro"}],
)
picker = DateTimePicker(name="deadline", label="Deadline")
TextInput—name,label, optionalhint_text,value, andvalidation.SelectionInput—name,label, anditems(value/text pairs).DateTimePicker—name,label, optionalvalue_ms_epochandtimezone_offset_date.
Validation enforces rules client-side per the documented JSON:
characterLimit (max input length) and inputType (TEXT, INTEGER,
FLOAT, EMAIL, EMOJI_PICKER).
Submitted widget values arrive back in event.form_inputs as typed
FormInputs values (StringInput, DateInput, DateTimeInput, TimeInput)
via the interaction parser, and Section.from_proto() rebuilds form widgets
from a raw SDK proto.
Sync card updates
A handler returning a Card:
- for MESSAGE events → {"cardsV2": [...]};
- for CARD_CLICKED → {"actionResponse": {"type": "UPDATE_MESSAGE"}, "cardsV2": [...]}.
UPDATE_MESSAGE is documented as «only permitted on a CARD_CLICKED event
where the message sender type is BOT». Updating cards on human messages
uses UPDATE_USER_MESSAGE_CARDS (implemented, sender-derived:
BOT → UPDATE_MESSAGE, HUMAN → UPDATE_USER_MESSAGE_CARDS; a MESSAGE with
a matched URL also updates via UPDATE_USER_MESSAGE_CARDS). Async card
updates go through bot.app.messages.update.