Skip to content

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.