Messages and Threads
Receive and reply
from chattice import Dispatcher, Router
from chattice.events import MessageEvent
router = Router()
@router.message()
async def echo(message: MessageEvent) -> str:
return f"You said: {message.text}"
dispatcher = Dispatcher()
dispatcher.include_router(router)
A returned string is an immediate interaction response. Use
message.argument_text when routing Space mentions: Google provides the
mention-stripped body, and Chattice removes its surrounding whitespace. The
raw message.text remains unchanged; None and an explicitly empty
argumentText remain distinct.
@router.message(F.argument_text.regexp(r"^[Rr]eport$"))
async def report(message: MessageEvent) -> str:
return "Generating report"
Contextual Chat API sends
Bind an authenticated Bot once:
dispatcher = Dispatcher(bot=bot)
@router.message()
async def send_variants(message: MessageEvent) -> str:
await message.reply("reply or fail if the incoming thread is gone")
assert message.thread is not None
await message.thread.send("continue the known thread")
assert message.space is not None
await message.space.send("start at the Space top level")
return "sent"
message.reply() defaults to the GAPIC
REPLY_MESSAGE_OR_FAIL reply option. Generic
thread.send() / space.send() default to Google's fallback-to-new-thread
behavior unless you pass another GAPIC reply option.
Imperative send
from google.apps.chat_v1.types import CreateMessageRequest
from chattice.client import Bot
from chattice.events import ThreadRef
await bot.app.messages.create("spaces/AAA", text="top-level")
await bot.app.messages.create(
"spaces/AAA",
text="threaded",
thread=ThreadRef(name="spaces/AAA/threads/T1"),
reply_option=(CreateMessageRequest.MessageReplyOption.REPLY_MESSAGE_OR_FAIL),
)
Use the imperative form in background jobs, Workspace Event handlers, or when the destination is not the current interaction Space.
Private message
The canonical private happy path is bot.app.messages.create(..., private_to=...):
await bot.app.messages.create(
"spaces/AAA",
text="Only User A and the app can see this",
private_to="users/user-a",
)
Private messages require app authentication. Chattice fails closed when the
viewer is empty or malformed and rejects incompatible accessory widgets before
network I/O. There is no private_reply helper.
Cards, notifications, IDs, and CRUD
messages.create also accepts card=, accessory_widgets=, notify=
("force" or "silent"), request_id=, and message_id=. Notification
options and accessory widgets require app auth. Private accessory widgets stay
unsupported because Google's published constraints conflict; use a private
plain-text/card message without accessory widgets.
message = await bot.app.messages.get("spaces/AAA/messages/M1")
await bot.app.messages.update(message.name, text="updated")
await bot.app.messages.delete(message.name)
Use request/message IDs for idempotent application workflows; HTTP delivery
can be retried. Read metadata such as attachments, annotations, mentions,
quote, reaction summaries, is_private, and is_silent from
MessageEvent. Fields without a curated facade remain available through
message.raw.
Google mapping: spaces.messages.create, get, update, and delete.
Next: Native commands.
Privacy model
Four distinct Google surfaces, chosen explicitly by the application:
| Surface | How | Visible to |
|---|---|---|
| Shared Space message | bot.app.messages.create(space) |
everyone in the Space |
| Thread reply | bot.app.messages.create(space, thread=...) or message.reply() |
everyone with access to the Space; a thread is not a privacy boundary |
| Private message | bot.app.messages.create(space, private_to=user) |
only privateMessageViewer (app auth required; no accessory widgets/attachments) |
| DM | send into the direct-message Space | only you and the app |
Dialogs are a separate synchronous surface: visible only to the opener, HTTP-only. App Home is the persistent personal surface (HTTP-only). Nothing is implicitly private and nothing implicitly becomes a Thread — the application chooses the surface per runtime capabilities.
Local files attach to messages via attachments=[InputFile(...)] —
upload requires USER authentication and private messages cannot carry
attachments. See Files, Images & Media.