Skip to content

Migrating bots from v1.X to v2.0

Version 2.0 uses the Backend API 20 event model and supports Python 3.11, 3.12 and 3.13. In this guide, v1.X means either of the supported versions v1.7 and v1.8. They expose the same documented public API, so the migration steps are identical. Bot code must be reviewed before installing v2.0.

1. Update Python and install the SDK

Versions 1.7 and 1.8 provided packages for Python 3.9, 3.10 and 3.11. Version 2.0 provides packages only for Python 3.11, 3.12 and 3.13.

If the bot runs on Python 3.9 or 3.10, upgrade Python first. Create a fresh virtual environment and install the matching v2.0 package from the SDK download.

2. Update chat membership event handlers

The event registration API is unchanged: continue to use Connection.set_handler() and Connection.start_notifications(). Update the registered event constants and their meaning as follows:

v1.X event v2.0 migration
CHAT_CREATED The event name and ChatChangeInfo payload class are unchanged. Review the payload compatibility notes below.
CHAT_DELETED The event name is unchanged and the SDK continues to emit one callback per affected chat. In v2.0, users contains the bot user that was removed; circles and roles are empty.
CHAT_JOINED Use CHAT_MEMBERS_ADDED for users, circles or roles added to a chat. In v2.0, CHAT_JOINED means that the bot itself was added to the chat.
CHAT_LEFT Replace with CHAT_MEMBERS_REMOVED for users, circles or roles removed from a chat.
CHAT_ALLOW_MUTE_NOTIFICATIONS_CHANGED Remove the handler; the setting and event are no longer part of the supported v2.0 public API.

CHAT_MEMBERS_ADDED and CHAT_MEMBERS_REMOVED use ChatMemberChangeInfo. Its users, circles and roles lists identify the affected direct members; lists that do not apply to an event are empty.

Circle membership has separate events:

  • CIRCLE_USERS_ADDED and CIRCLE_USERS_REMOVED report users added to or removed from a circle that belongs to a chat. Their payload is CircleMemberUpdateInfo.
  • CIRCLES_DELETED reports circles deleted and removed from a chat.

This is different from adding or removing the circle itself, which produces CHAT_MEMBERS_ADDED or CHAT_MEMBERS_REMOVED. The SDK keeps its local chat membership state updated as these events arrive.

Role membership has the same distinction:

  • Adding or removing a role as a direct chat member produces CHAT_MEMBERS_ADDED or CHAT_MEMBERS_REMOVED.
  • ROLES_JOINED and ROLES_LEFT report a user joining or leaving roles that belong to a chat.
  • ROLE_USERS_REMOVED reports users removed from a role, and ROLES_DELETED reports roles deleted and removed from a chat.

These role events also use ChatMemberChangeInfo; its roles and users lists identify the affected objects.

For example, a comprehensive replacement for a v1.X subscription to CHAT_JOINED and CHAT_LEFT is:

MEMBERSHIP_EVENTS = [
    Events.CHAT_MEMBERS_ADDED,
    Events.CHAT_MEMBERS_REMOVED,
    Events.CIRCLE_USERS_ADDED,
    Events.CIRCLE_USERS_REMOVED,
    Events.CIRCLES_DELETED,
    Events.ROLES_JOINED,
    Events.ROLES_LEFT,
    Events.ROLE_USERS_REMOVED,
    Events.ROLES_DELETED,
]

connection.set_handler(MEMBERSHIP_EVENTS, membership_handler)

See the v2.0 events reference for the payload of each event.

3. Replace lists and review circle usage

The SDK no longer exposes user-created lists because bots cannot own lists. In v2.0, the existing public names Group, connection.groups and gid refer only to circles; gid is the circle's legacy ID.

Remove code that uses:

  • GroupType, Group.type, Group.is_list, Group.is_managed_group or Group.is_user_group
  • connection.groups.create_group()
  • Group.add_members() or Group.remove_members()

There is no bot-side replacement for creating a list or changing its members. Depending on the bot's purpose, use direct users, administrator-managed public circles, or roles as chat members instead.

connection.groups.get_list() can return both public and private circles, but bots can only add or remove public circles as chat members. Check Group.is_public before passing a circle through the groups argument of a chat method.

If a bot used undocumented SDK internals, also remove uses of Group.groups, Group.get_groups() and Chat.chat_group. API 20 stores direct users, circles and roles on the chat instead of exposing the former internal chat group.

4. Update chat creation and settings

Chats.create_chat() now accepts roles before message, and removes is_alert and allow_mute_notifications. Calls that pass arguments after groups positionally can therefore change meaning. Convert them to keyword arguments:

chat = await connection.chats.create_chat(
    title='Operations',
    users=users,
    groups=public_circles,
    message='Welcome',
    admins=admins,
    allow_sending=True,
)

Also make these changes where applicable:

  • Remove uses of Chat.is_alert and the is_alert argument to create_chat(). Alert messages remain supported through Chat.post(is_alert=True, alert_type=...).
  • Remove uses of Chat.allow_mute_notifications, Chat.set_allow_mute_notifications() and the corresponding create_chat() argument. There is no replacement bot-controlled setting.
  • Existing Chat.add_members() and Chat.remove_members() calls using direct users or public circles still work. Both methods now also accept roles=[].
  • Do not remove the bot by passing connection.own_user to Chat.remove_members(). This now raises TWIllegalArgument; use Chat.leave() instead.
  • Chat.leave() can raise TWAuthError when the bot belongs to the chat only indirectly through a circle or role. Resolve that indirect membership by changing the contributing circle or role administratively, or, where permitted, by removing that circle or role from the chat.
  • Member updates now refresh and replace the direct membership atomically. If an update can race with another bot, user or administrator, handle the new TWConflictError and reconcile against the latest membership before retrying. An update can also raise TWNotFoundError if the chat disappears while it is being refreshed.

5. Adopt roles only if needed

Bots that do not need role-aware behavior can ignore role-specific objects and events. To use roles, check Permissions.roles_enabled, access them through connection.roles, and pass Role objects through roles= when creating or updating chats.

Use await connection.roles.get_list() or await connection.roles.get_role(role_id) to retrieve roles. Chat.get_role_members() returns the roles that are direct members of a chat; use await Role.get_members() to resolve the visible users in a role. Chat.get_user_members() continues to return only direct user members and does not include users inherited through circles or roles.

Incoming Message objects now have sender_roles, a list of role names frozen when the message was sent. These are strings, not Role objects, and they do not represent the sender's current role membership.

Permissions.roles_settings contains optional backend role configuration when roles are enabled. Do not assume it is present. See the roles API reference and Working with roles for examples.

6. Review notification payload access

The documented event handler signature is unchanged: event handlers still receive the event constant and, where applicable, an information object. In v2.0, notification information objects are dataclasses with declared attributes. Attributes that do not apply to a particular event can therefore be present with a value of None or an empty list. Use the event constant to distinguish events instead of testing only with hasattr().

Remove uses of deprecated v1.X payload compatibility attributes as follows:

  • Replace extra.thread_id with extra.chat.tid.
  • Replace extra.user_ids with user IDs derived from extra.users.
  • Replace extra.editor_id with extra.editor.uid, allowing for editor to be None.
  • For MESSAGE_READ, replace the deprecated dictionary-style extra.get(...) or extra[...] access with the documented chat, user, message_id and read_date attributes.

7. Review custom asyncio event-loop setup

The Connection constructor now defaults to loop=None. When no loop is provided, v2.0 creates a new asyncio event loop and installs it as the current loop. Bots that manage their own loop, or create tasks on a loop before creating the connection, should pass that loop explicitly through Connection(loop=...).

8. Keep handler code, but review notification network configuration

The SDK now receives notifications over WebSockets instead of Crossbar. This is an internal change: Connection.set_handler(), start_notifications() and connection event handlers keep their public interfaces, so ordinary bot handler code does not need transport-specific changes.

Deployments with custom proxies or firewall rules must allow the API 20 WebSocket endpoint derived from TW_API_HOST at /ws/v20/. The legacy TW_API_WS_URL override is not used by v2.0; update any deployment that relied on it.