Memberships and Spaces
Bot exposes the Google Chat membership primitives needed to add, inspect,
list, and remove human users. It also lists the Spaces visible to the
Chat app. Business concepts such as departments or groups of Spaces remain in
the application.
Configure APP authentication
Membership mutation is an APP-auth operation. Configure the service account
with both chat.bot and chat.app.memberships when the same Bot also sends
messages and lists Spaces:
from chattice.auth import (
CHAT_APP_MEMBERSHIPS_SCOPE,
CHAT_BOT_SCOPE,
ServiceAccountCredentialsProvider,
)
from chattice.client import Bot
provider = ServiceAccountCredentialsProvider.from_service_account_file(
"/run/secrets/chat-service-account.json",
scopes=[CHAT_BOT_SCOPE, CHAT_APP_MEMBERSHIPS_SCOPE],
)
bot = Bot(credentials_provider=provider)
chat.app.memberships requires one-time approval from a Google Workspace
administrator. Google currently marks
create
and delete
with app authentication as Developer Preview. Declaring the scope in code does
not grant approval or enroll an account. Membership operations never select
the Bot's USER identity and do not reuse attachment/OAuth credentials.
App authentication can create or remove memberships for human users. It cannot use these helpers to add external users, Google Groups, or other Chat apps. Google can also reject removal of a Space manager unless the Chat app created the Space.
Add and inspect a member
Bare Space IDs and canonical resource names are both accepted. A user can be
passed as a canonical users/... resource name:
membership = await bot.app.memberships.create(
"spaces/AAA",
user="users/123",
)
same_membership = await bot.app.memberships.get(
"spaces/AAA/members/users/123",
)
The returned value is Google's Membership proto. Chattice does not hide its
state, role, invitation state, or future SDK fields.
List memberships
memberships.list() returns a Pager; iterate it with async for (pages
are fetched as you go) or collect() for one list. filter is
passed to Google unchanged; Chattice does not invent a filtering DSL.
pager = await bot.app.memberships.list(
parent="spaces/AAA",
filter='member.type = "HUMAN" AND role = "ROLE_MEMBER"',
)
async for membership in pager:
...
Membership get/list use APP authentication and accept either chat.bot or
chat.app.memberships. Create/delete require chat.app.memberships.
Remove directly by user resource name
No preliminary list or get is required:
removed = await bot.app.memberships.delete(
"spaces/AAA/members/users/123",
)
memberships.delete() is not silently idempotent. If the membership does not
exist, Chattice raises ChatNotFoundError through the normal Chat API error
policy. Application code may choose to treat that as a skip for a bulk job.
List Spaces and perform a bulk removal
spaces.list() uses APP authentication with chat.bot and yields (a
Pager) the Spaces in which the calling Chat app is a member. It does not use
spaces.search(), which also supports searching the calling user's spaces
without administrator privileges. Use the raw SDK for search.
from chattice.client import (
ChatAlreadyExistsError,
ChatAPIError,
ChatNotFoundError,
)
spaces = await bot.app.spaces.list(filter='spaceType = "SPACE"')
failures = []
async for space in spaces:
try:
await bot.app.memberships.delete(f"{space.name}/members/users/123")
except ChatNotFoundError:
continue
except ChatAPIError as error:
failures.append((space.name, error))
All membership and Space-list methods wrap Google SDK failures in the
ChatAPIError hierarchy. Catch ChatNotFoundError when a membership is
absent and ChatAlreadyExistsError when an add operation reports HTTP 409;
permission, validation, rate-limit, authentication, and service failures
have their corresponding typed subclasses. Each wrapper preserves the SDK
exception as error.__cause__ and exposes its code and details values.
The hierarchy is:
ChatAPIError
├── ChatNotFoundError
├── ChatAlreadyExistsError
├── ChatPermissionDeniedError
├── ChatInvalidArgumentError
├── ChatRateLimitError
├── ChatServiceUnavailableError
└── ChatUnauthenticatedError
try:
await bot.app.memberships.create("spaces/AAA", user="users/123")
except ChatAlreadyExistsError:
pass # application policy: the desired membership is already present
except ChatNotFoundError:
pass # application policy: the Space was removed or is not visible
Google doesn't list empty group chats and direct messages until their first
message. Filtering for spaceType = "SPACE" is the intended way to restrict a
bulk operation to named Spaces visible to the app.
Mappings such as "sales" -> ["spaces/A", "spaces/B"], retries, concurrency,
already-member handling, and result summaries belong to application code.