Skip to content

aiogram comparison

For developers coming from aiogram, chattice is an async Python framework for Google Chat — not a Telegram-compatible reimplementation. This is a semantic comparison of concepts, not a compatibility promise. Every chattice claim below is grounded in the shipped API; where Google Chat lacks a Telegram feature, this page says so explicitly.

aiogram concept chattice concept Google Chat limitation
chat_id SpaceRef/ThreadRef resource names (events.references) Spaces are the addressing unit, identified by resource names like spaces/AAA...; direct messages are spaces too
update_id no update IDs No documented update ordering API; ordering is not guaranteed
inline queries not implemented No Google Chat equivalent
callback_data button action function name + string parameters (cards.Action) Action parameters are string key/value pairs; no opaque callback payload
message.answer() handler return, MessageEvent.reply(), ThreadRef.send(), SpaceRef.send(), or bot.app.messages.create() A sync HTTP response and an authenticated Chat API call are different channels
reply keyboard Cards (buttons) (cards) Cards can only be updated via updateMessage for messages the bot created
dialogs anywhere dialogs open only in response to an interaction (OPEN_DIALOG) Dialogs are visible only to the user who triggered the interaction
polling HTTP push / Pub/Sub push ingress + Pub/Sub STREAMING PULL No Telegram-style long polling; streaming pull is a persistent subscriber stream with Pub/Sub's own ACK protocol (see pubsub)
FSM chat_id keys StorageKey(user, space, thread) (fsm.storage) Keys are Google resource references, never chat IDs

What you keep from aiogram

The developer experience that made aiogram productive carries over:

  • Routers — feature-scoped handler registration on a shared Dispatcher.
  • Magic filters — the F predicate DSL, reimplemented over Google-native fields (no Telegram fields leak into it).
  • MiddlewareDispatcher/Router middleware with the same outer-to-inner ordering semantics.
  • Dependency injection — context-injected handlers; the InteractionContext (request snapshot, response, sync deadline) is injected like aiogram's per-event context.
  • FSMStatesGroup, StateFilter, memory/Redis storage, and StorageKey-based scoping (USER_IN_SPACE, USER, SPACE).

The ergonomics survive; the event vocabulary does not.

What Google Chat simply does not have

The following Telegram concepts have no Google Chat equivalent, and chattice does not pretend they exist:

  • Inline queries — nothing like Telegram's inline mode.
  • Update ordering — no update_id-style sequence and no documented ordering guarantee, so no update_id-based deduplication.
  • Telegram-style long polling — no getUpdates-style polling. Ingress is push (HTTP webhook / Pub/Sub push) or Pub/Sub STREAMING PULL (dispatcher.run_pubsub), which is a persistent subscriber stream, not a polling loop.
  • User-updatable messages — cards can be edited via updateMessage, but only if the bot created the message.
  • Context menus / global dialogs — dialogs only open in direct response to an interaction and are visible only to the opener.

Where a Telegram feature has no Google Chat counterpart, the honest answer is "not available", not a reimplementation with a different name.

Local files: FSInputFile vs InputFile

AIOGRAM:

from aiogram.types import FSInputFile

await message.reply_document(FSInputFile(path="report.pdf"))

CHATTICE:

from chattice.media import InputFile

await bot.user.messages.create(
    message.space,
    thread=message.thread,
    attachments=[InputFile.from_path("report.pdf")],
)

One canonical InputFile covers every local artifact — there are no Photo/Document/Video entities. InputFile.from_bytes(data, filename=...) wraps in-memory content; from_path reads lazily, only on the upload path.

Google Chat media upload requires USER auth

A service-account Chat app cannot upload a local file — Google restricts media.upload to user authentication. Configure one Bot with both identities (Bot(app_credentials_provider=..., user_credentials_provider=...)) and call bot.user.messages.create(..., attachments=...) explicitly. Contextual message.reply() uses APP auth. A hosted HTTPS image in a Card (chattice.cards.Image) is the app-auth alternative. See Files, Images & Media.

Proactive sends: chat_id vs Space

AIOGRAM:

await bot.send_message(chat_id=FINANCE_CHAT, text="New request #431")

CHATTICE:

await bot.app.messages.create(FINANCE_SPACE, text="New request #431")

Multi-target (ONE business event → SEVERAL Spaces):

AIOGRAM:

await bot.send_message(chat_id=A, text=...)
await bot.send_message(chat_id=B, text=...)

CHATTICE:

await bot.app.messages.create(A, text=...)
await bot.app.messages.create(B, text=...)

Business logic (CRM, APIs, databases, screenshot/report generation, Jira/Asana) does NOT know about Google Chat in either framework — only the messenger-facing calls change. See the proactive outbound section of examples/docs/from_zero.py.

The reply mental model

Chattice provides contextual send methods without hiding Google's two channels. A handler return is the current interaction response; contextual and imperative send methods use the authenticated Chat API:

return "pong"  # interaction response
await message.reply("pong")  # authenticated reply in the known thread
await message.thread.send("pong")  # authenticated explicit thread send
await message.space.send("pong")  # authenticated Space-level send
await bot.app.messages.create("spaces/AAA", text="pong")  # imperative outbound

The contextual methods are zero-fetch adapters over the same bound Bot.

Mention routing note: Google can leave surrounding whitespace in argumentText after removing the app mention. Chattice strips that surrounding whitespace while preserving message.text exactly as received. A Space message @AppName ping can therefore route as F.argument_text == "ping" without a whitespace-aware application regex.

The same skeleton, two transports

Your aiogram echo bot:

from aiogram import Bot, Dispatcher
from aiogram.types import Message

bot = Bot(token=TOKEN)
dp = Dispatcher()


@dp.message()
async def echo(message: Message) -> None:
    await message.answer(message.text)


async def main() -> None:
    await dp.start_polling(bot)

Chattice, HTTP (Google calls your HTTPS endpoint; the handler RETURN is the synchronous response):

import os
from fastapi import FastAPI
from chattice import Dispatcher, Router
from chattice.events import MessageEvent
from chattice.integrations.fastapi import create_chat_router
from chattice.transports.http import GoogleTokenVerifier

router = Router()


@router.message()
async def echo(message: MessageEvent) -> str:
    return f"You said: {message.text}"  # synchronous response; no outbound credentials


dispatcher = Dispatcher()
dispatcher.include_router(router)

app = FastAPI()
app.include_router(
    create_chat_router(
        dispatcher,
        GoogleTokenVerifier(audience=os.environ["CHATTICE_AUDIENCE"]),
    )
)
# python -m uvicorn app:app --port 8000

Chattice, Pub/Sub streaming pull (persistent runner, like start_polling in shape; no domain, no TLS):

import asyncio
import os
from chattice import Dispatcher, Router
from chattice.auth import ServiceAccountCredentialsProvider
from chattice.client import Bot
from chattice.events import MessageEvent

router = Router()


@router.message()
async def echo(message: MessageEvent) -> None:
    await message.reply(f"You said: {message.text}")  # no sync return on pull


async def main() -> None:
    service_account_file = os.environ["CHATTICE_SERVICE_ACCOUNT_FILE"]
    app_credentials = ServiceAccountCredentialsProvider.from_service_account_file(
        service_account_file
    )
    pull_credentials = ServiceAccountCredentialsProvider.from_service_account_file(
        service_account_file, scopes=["https://www.googleapis.com/auth/pubsub"]
    )
    async with Bot(app_credentials_provider=app_credentials) as bot:
        dispatcher = Dispatcher(bot=bot)
        dispatcher.include_router(router)
        await dispatcher.run_pubsub(
            os.environ["CHATTICE_SUBSCRIPTION"],
            bot=bot,
            credentials_provider=pull_credentials,
        )


if __name__ == "__main__":
    asyncio.run(main())
# python app.py

Correspondences: start_polling(bot)run_pubsub(subscription, bot=bot, credentials_provider=...); aiogram webhook ↔ the HTTP variant above. Pass the Bot to the pull runner explicitly, even when the Dispatcher was constructed with bot=bot. The Dispatcher inherits Router observers, so direct registration is supported; separate Routers keep handlers reusable.