Mike před 4 měsíci
rodič
revize
c318f3e2a1
1 změnil soubory, kde provedl 1828 přidání a 0 odebrání
  1. 1828 0
      docs/TOC.md

+ 1828 - 0
docs/TOC.md

@@ -0,0 +1,1828 @@
+# TOC Protocol Specification
+
+Version: TOC 1.0 / TOC 2.0
+
+This document is the authoritative reference for the TOC (Talk to OSCAR
+Client) protocol as implemented by Open OSCAR Server. It covers both TOC 1.0
+and TOC 2.0, and contains enough detail to build a fully-functional client
+from scratch.
+
+TOC is an ASCII-based protocol layered on top of FLAP (SFLAP) framing over
+TCP. It was originally designed by AOL to service the TiK (Tcl/Tk) and TIC
+(Java) AIM clients. TOC 2.0 extends the original protocol with server-side
+buddy lists, typing notifications, encoded messages, and richer buddy status
+information.
+
+---
+
+## Table of Contents
+
+1. [Transport Layer (FLAP)](#1-transport-layer-flap)
+2. [Connection & Authentication](#2-connection--authentication)
+3. [Client → Server Commands (TOC 1)](#3-client--server-commands-toc-1)
+4. [Client → Server Commands (TOC 2)](#4-client--server-commands-toc-2)
+5. [Server → Client Messages (TOC 1)](#5-server--client-messages-toc-1)
+6. [Server → Client Messages (TOC 2)](#6-server--client-messages-toc-2)
+7. [Error Codes](#7-error-codes)
+8. [Escaping & Encoding Rules](#8-escaping--encoding-rules)
+9. [Capabilities (UUIDs)](#9-capabilities-uuids)
+10. [Complete Session Examples](#10-complete-session-examples)
+
+---
+
+## 1. Transport Layer (FLAP)
+
+All TOC communication is wrapped in FLAP (SFLAP) frames over a single TCP
+connection. The default port is **9898**.
+
+### 1.1 FLAP Header
+
+Every frame begins with a 6-byte binary header in **network byte order**
+(big-endian):
+
+| Offset | Size | Field | Description |
+|--------|------|-------|-------------|
+| 0 | 1 | Marker | Always `0x2A` (ASCII `*`) |
+| 1 | 1 | Frame Type | See below |
+| 2 | 2 | Sequence Number | Monotonically increasing per direction |
+| 4 | 2 | Data Length | Length of the payload that follows |
+
+The payload immediately follows the header at offset 6.
+
+### 1.2 Frame Types
+
+| Value | Name | Description |
+|-------|------|-------------|
+| 1 | SIGNON | Connection initialization |
+| 2 | DATA | Normal command/response data |
+| 3 | ERROR | Not used by TOC |
+| 4 | SIGNOFF | Client disconnect |
+| 5 | KEEP_ALIVE | Heartbeat (no payload) |
+
+### 1.3 SIGNON Frame Payload
+
+**Server → Client** (after the client sends `FLAPON`):
+
+| Offset | Size | Description |
+|--------|------|-------------|
+| 0 | 4 | FLAP version (always `0x00000001`) |
+
+**Client → Server** (response to the server's signon frame):
+
+| Offset | Size | Description |
+|--------|------|-------------|
+| 0 | 4 | FLAP version (`0x00000001`) |
+| 4 | 2 | TLV tag (`0x0001`) |
+| 6 | 2 | Screen name length (N) |
+| 8 | N | Normalized screen name (NOT null-terminated) |
+
+### 1.4 DATA Frame Payload
+
+- **Client → Server:** The payload is the TOC command as a null-terminated
+  ASCII string. The Data Length includes the null terminator.
+- **Server → Client:** The payload is the TOC response as an ASCII string.
+  It is NOT null-terminated. The Data Length is the exact string length.
+
+### 1.5 Sequence Numbers
+
+Each direction maintains an independent sequence counter starting at an
+arbitrary value (usually 0 for clients). The counter increments by 1 for each
+frame sent. When it reaches 65535 it wraps to 0.
+
+### 1.6 Maximum Lengths
+
+- **Client → Server:** 2048 bytes maximum per command. Exceeding this drops
+  the connection.
+- **Server → Client:** 8192 bytes maximum per message.
+
+---
+
+## 2. Connection & Authentication
+
+### 2.1 Connection Handshake
+
+```
+Client                              Server
+  |                                    |
+  |  --- TCP connect (port 9898) --->  |
+  |  --- "FLAPON\r\n\r\n" --------->  |
+  |  <-- FLAP SIGNON frame ---------- |
+  |  --- FLAP SIGNON frame --------->  |
+  |  --- toc_signon/toc2_login -----> |
+  |  <-- SIGN_ON --------------------- |
+  |  <-- CONFIG/CONFIG2 -------------- |
+  |  <-- NICK ------------------------ |
+  |  --- toc_add_buddy (optional) ---> |
+  |  --- toc_add_permit (optional) --> |
+  |  --- toc_init_done --------------> |
+  |  <-- UPDATE_BUDDY (for each   --- |
+  |       online buddy)                |
+```
+
+Step by step:
+
+1. Open a TCP connection to the server (default port 9898).
+2. Send the string `FLAPON\r\n\r\n` (10 bytes). Alternatively, `FLAPON\n\n`
+   (8 bytes) is also accepted.
+3. Server responds with a FLAP SIGNON frame (frame type 1) containing the
+   4-byte FLAP version.
+4. Client sends a FLAP SIGNON frame back containing: the 4-byte FLAP version,
+   a TLV with tag `0x0001` holding the normalized screen name.
+5. Client sends the login command as a FLAP DATA frame (frame type 2).
+6. On success, the server sends `SIGN_ON`, then `CONFIG`/`CONFIG2`, then
+   `NICK`. On failure, the server sends an `ERROR` and/or drops the
+   connection.
+7. Client optionally sends buddy list and permit/deny setup.
+8. Client sends `toc_init_done` to go online. This MUST be sent within 30
+   seconds of the signon command.
+
+### 2.2 Password Roasting
+
+Passwords are "roasted" before transmission to prevent cleartext on the wire
+(this is obfuscation, not encryption).
+
+**Roasting String:** `Tic/Toc`
+
+**Algorithm:**
+
+1. XOR each byte of the password with the corresponding byte of the roasting
+   string (cycling through the string).
+2. Convert the result to lowercase hexadecimal.
+3. Prepend `0x`.
+
+**Example:** The password `password` roasts to `0x2408105c23001130`.
+
+```python
+def roast_password(password):
+    roast = "Tic/Toc"
+    result = []
+    for i, ch in enumerate(password):
+        xored = ord(ch) ^ ord(roast[i % len(roast)])
+        result.append(f"{xored:02x}")
+    return "0x" + "".join(result)
+```
+
+### 2.3 TOC 1 Signon
+
+```
+toc_signon <authorizer_host> <authorizer_port> <screen_name> <roasted_password> <language> <version>
+```
+
+| Parameter | Description |
+|-----------|-------------|
+| authorizer_host | Auth server hostname (e.g. `login.oscar.aol.com`). Ignored by Open OSCAR Server but required. |
+| authorizer_port | Auth server port (e.g. `5190`). Ignored but required. |
+| screen_name | Normalized screen name |
+| roasted_password | Password roasted as described above |
+| language | Language string (e.g. `english`) |
+| version | Client version string, max 50 characters (e.g. `"TIC:MyClient"`) |
+
+**Example:**
+
+```
+toc_signon login.oscar.aol.com 5190 toctest1 0x2408105c23001130 english "TIC:MyClient 1.0"
+```
+
+On success, the server replies with:
+
+```
+SIGN_ON:TOC1.0
+CONFIG:<config data>
+NICK:<formatted screen name>
+```
+
+### 2.4 TOC 2 Signon (toc2_signon)
+
+Same syntax as `toc_signon`. The server replies with `SIGN_ON:TOC2.0` and
+`CONFIG2` instead of `CONFIG`. TOC 2 features (server-side buddy list,
+`UPDATE_BUDDY2`, `IM_IN2`, etc.) are enabled.
+
+```
+toc2_signon login.oscar.aol.com 5190 toctest1 0x2408105c23001130 english "TIC:MyClient 1.0"
+```
+
+### 2.5 TOC 2 Login (toc2_login)
+
+Extends `toc2_signon` with additional trailing parameters and enables encoded
+message variants (`IM_IN_ENC2`, `CHAT_IN_ENC`).
+
+```
+toc2_login <host> <port> <screen_name> <roasted_pw> <language> <version> 160 US "" "" 3 0 30303 -kentucky -utf8 <code>
+```
+
+| Parameter | Description |
+|-----------|-------------|
+| (first 6) | Same as `toc_signon` |
+| 160 US "" "" 3 0 30303 -kentucky -utf8 | Fixed parameters (send verbatim) |
+| code | Login code: `7696 * ascii(screen_name[0]) * ascii(password[0])` |
+
+The version string MUST start with `TIC:` (e.g. `"TIC:MyClient"`).
+
+**Code calculation example:**
+
+```python
+def login_code(screen_name, password):
+    return 7696 * ord(screen_name[0]) * ord(password[0])
+```
+
+For screen name `toctest1` (first char `t` = 116) and password `testpass1`
+(first char `t` = 116): `7696 * 116 * 116 = 103,547,776`.
+
+**Example:**
+
+```
+toc2_login login.oscar.aol.com 5190 toctest1 0x2408105c23001130 english "TIC:MyClient" 160 US "" "" 3 0 30303 -kentucky -utf8 103547776
+```
+
+On success:
+
+```
+SIGN_ON:TOC2.0
+NICK:toctest1
+CONFIG2:<config data>
+```
+
+---
+
+## 3. Client → Server Commands (TOC 1)
+
+All client commands are lowercase, space-separated, and sent as FLAP DATA
+frames (null-terminated). Arguments containing spaces should be enclosed in
+quotes. Screen names must be **normalized** (lowercased, spaces removed).
+Message content must be **escaped** (see [Escaping Rules](#8-escaping--encoding-rules)).
+
+### toc_init_done
+
+Tells the server the client is ready to go online. Must be sent within 30
+seconds of the signon command, and only after `SIGN_ON` is received.
+
+```
+toc_init_done
+```
+
+### toc_send_im
+
+Send an instant message to a user. The message may contain basic HTML.
+
+```
+toc_send_im <destination_user> <message> [auto]
+```
+
+| Parameter | Description |
+|-----------|-------------|
+| destination_user | Normalized screen name of the recipient |
+| message | Quoted, escaped message (may contain HTML) |
+| auto | Optional literal string `auto` to mark as auto-response |
+
+**Examples:**
+
+```
+toc_send_im toctest2 "Hello there!"
+toc_send_im toctest2 "I am away right now" auto
+toc_send_im toctest2 "Check out \$100 deals"
+```
+
+### toc_add_buddy
+
+Add one or more buddies to the buddy list. This subscribes to their presence
+updates. Does not modify saved config (TOC 1 only — use `toc_set_config` to
+persist).
+
+```
+toc_add_buddy <buddy1> [<buddy2> ...]
+```
+
+**Example:**
+
+```
+toc_add_buddy toctest2 joe mike
+```
+
+### toc_remove_buddy
+
+Remove one or more buddies from the buddy list. Does not modify saved config.
+
+```
+toc_remove_buddy <buddy1> [<buddy2> ...]
+```
+
+**Example:**
+
+```
+toc_remove_buddy toctest2
+```
+
+### toc_set_config
+
+Save the buddy list configuration on the server. The config is a line-oriented
+format where the first character is the item type, followed by a space, then
+the value. Enclose the entire config in quotes.
+
+```
+toc_set_config <config_string>
+```
+
+**Config item types:**
+
+| Prefix | Description |
+|--------|-------------|
+| `g` | Buddy group name. All buddies until the next `g` or end of config belong to this group. |
+| `b` | Buddy screen name |
+| `p` | Permit list entry |
+| `d` | Deny list entry |
+| `m` | Permit/Deny mode: `1`=Permit All, `2`=Deny All, `3`=Permit Some, `4`=Deny Some |
+
+**Example:**
+
+```
+toc_set_config "m 1
+g Buddies
+b toctest2
+b joe
+g Coworkers
+b mike
+d spammer123
+"
+```
+
+### toc_get_status
+
+Query the online status of a user. Returns an `UPDATE_BUDDY` message if
+online, or an `ERROR` if the user is not available.
+
+```
+toc_get_status <screen_name>
+```
+
+**Example:**
+
+```
+toc_get_status toctest2
+```
+
+### toc_set_info
+
+Set the user's profile information (HTML).
+
+```
+toc_set_info <info_html>
+```
+
+**Example:**
+
+```
+toc_set_info "<b>Hello!</b> I love AIM."
+```
+
+### toc_get_info
+
+Request a user's profile. The server responds with a `GOTO_URL` containing a
+URL to view the profile, or an `ERROR`.
+
+```
+toc_get_info <screen_name>
+```
+
+**Example:**
+
+```
+toc_get_info toctest2
+```
+
+### toc_set_away
+
+Set or clear the away message. If a message is provided, the user's
+unavailable flag is set. If omitted, the flag is cleared.
+
+```
+toc_set_away [<away_message>]
+```
+
+**Examples:**
+
+```
+toc_set_away "Gone fishing! <b>BRB</b>"
+toc_set_away
+```
+
+### toc_set_idle
+
+Set the idle time in seconds. If 0, the user is marked as active. The server
+auto-increments the idle counter, so only call this once.
+
+```
+toc_set_idle <seconds>
+```
+
+**Examples:**
+
+```
+toc_set_idle 300
+toc_set_idle 0
+```
+
+### toc_set_caps
+
+Declare the client's capabilities. All supported capabilities must be sent at
+once. Capabilities are represented as UUIDs (see
+[Capabilities](#9-capabilities-uuids)).
+
+```
+toc_set_caps [<uuid1> [<uuid2> ...]]
+```
+
+**Example:**
+
+```
+toc_set_caps 09461343-4C7F-11D1-8222-444553540000 09461345-4C7F-11D1-8222-444553540000
+```
+
+### toc_evil
+
+Warn (evil) another user. You can only warn users who have recently sent you
+an IM.
+
+```
+toc_evil <screen_name> <norm|anon>
+```
+
+| Type | Effect |
+|------|--------|
+| `norm` | Normal warning (+20% to target's warning level, your identity revealed) |
+| `anon` | Anonymous warning (+10%, your identity hidden) |
+
+**Example:**
+
+```
+toc_evil spammer123 anon
+```
+
+### toc_add_permit
+
+Add users to the permit list. If currently in deny mode, switches to permit
+mode first. With no arguments and in deny mode, switches to permit-none.
+
+```
+toc_add_permit [<user1> [<user2> ...]]
+```
+
+**Example:**
+
+```
+toc_add_permit toctest2 joe
+```
+
+### toc_add_deny
+
+Add users to the deny (block) list. If currently in permit mode, switches to
+deny mode first. With no arguments and in permit mode, switches to deny-none.
+
+```
+toc_add_deny [<user1> [<user2> ...]]
+```
+
+**Example:**
+
+```
+toc_add_deny spammer123
+```
+
+### toc_chat_join
+
+Join a chat room on the specified exchange. Exchange should be `4` for
+standard chat rooms. The server responds with `CHAT_JOIN` on success or
+`ERROR` on failure.
+
+```
+toc_chat_join <exchange> <chat_room_name>
+```
+
+**Example:**
+
+```
+toc_chat_join 4 "My Chat Room"
+```
+
+### toc_chat_send
+
+Send a message to a chat room. Use the chat room ID received in the
+`CHAT_JOIN` response. Since reflection is always on, you will receive your own
+message back as a `CHAT_IN`.
+
+```
+toc_chat_send <chat_room_id> <message>
+```
+
+**Example:**
+
+```
+toc_chat_send 0 "Hello everyone!"
+```
+
+### toc_chat_whisper
+
+Send a private message within a chat room directed at a specific user.
+
+```
+toc_chat_whisper <chat_room_id> <destination_user> <message>
+```
+
+**Example:**
+
+```
+toc_chat_whisper 0 toctest2 "Can you see this?"
+```
+
+### toc_chat_invite
+
+Invite one or more users to a chat room you are currently in.
+
+```
+toc_chat_invite <chat_room_id> <invite_message> <buddy1> [<buddy2> ...]
+```
+
+**Example:**
+
+```
+toc_chat_invite 0 "Come join us!" toctest2 joe
+```
+
+### toc_chat_accept
+
+Accept a chat invitation received via `CHAT_INVITE`. The server will respond
+with `CHAT_JOIN`.
+
+```
+toc_chat_accept <chat_room_id>
+```
+
+**Example:**
+
+```
+toc_chat_accept 5
+```
+
+### toc_chat_leave
+
+Leave a chat room.
+
+```
+toc_chat_leave <chat_room_id>
+```
+
+**Example:**
+
+```
+toc_chat_leave 0
+```
+
+### toc_set_dir
+
+Set directory information. Fields are colon-separated within a quoted string.
+
+```
+toc_set_dir <info_string>
+```
+
+**Fields (colon-separated):** `"first_name":"middle_name":"last_name":"maiden_name":"city":"state":"country":"email":"allow_web_search"`
+
+**Example:**
+
+```
+toc_set_dir "John":"":"Doe":"":"New York":"NY":"US":"john@example.com":""
+```
+
+### toc_get_dir
+
+Get a user's directory information. Returns a `GOTO_URL` or `ERROR`.
+
+```
+toc_get_dir <screen_name>
+```
+
+**Example:**
+
+```
+toc_get_dir toctest2
+```
+
+### toc_dir_search
+
+Search the user directory by field. Returns a `GOTO_URL` or `ERROR`.
+
+```
+toc_dir_search <search_fields>
+```
+
+**Fields (colon-separated):** `"first_name":"middle_name":"last_name":"maiden_name":"city":"state":"country":"email"`
+
+You can search by keyword by placing it in the 11th position:
+
+```
+toc_dir_search "::::::::":"":"search term"
+```
+
+**Example:**
+
+```
+toc_dir_search "John":"":"Doe":"":"":"":"":""
+```
+
+### toc_format_nickname
+
+Reformat the capitalization/spacing of your screen name. Returns
+`ADMIN_NICK_STATUS` and `NICK` on success, or `ERROR` on failure.
+
+```
+toc_format_nickname <new_format>
+```
+
+**Example:**
+
+```
+toc_format_nickname "TocTest1"
+```
+
+### toc_change_passwd
+
+Change your password. Returns `ADMIN_PASSWD_STATUS` on success or `ERROR` on
+failure. Passwords are sent as plaintext (not roasted). Special characters
+must be backslash-escaped (see [Escaping Rules](#8-escaping--encoding-rules)).
+
+Note: Because passwords are sent in cleartext (unlike the roasted password at
+login), this command is not secure against network eavesdropping.
+
+```
+toc_change_passwd <old_password> <new_password>
+```
+
+**Examples:**
+
+```
+toc_change_passwd oldpass newpass
+toc_change_passwd "my old pass" "my new pass"
+toc_change_passwd oldpa\$\$ newpa\$\$
+```
+
+### toc_rvous_accept
+
+Accept a rendezvous proposal (e.g. file transfer) from another user.
+
+```
+toc_rvous_accept <screen_name> <cookie> <service_uuid>
+```
+
+| Parameter | Description |
+|-----------|-------------|
+| screen_name | The user who sent the proposal |
+| cookie | Base64-encoded cookie from the `RVOUS_PROPOSE` message |
+| service_uuid | UUID of the service being accepted |
+
+**Example:**
+
+```
+toc_rvous_accept toctest2 fUdgNuQ6AAA= 09461343-4C7F-11D1-8222-444553540000
+```
+
+### toc_rvous_cancel
+
+Cancel/decline a rendezvous proposal from another user.
+
+```
+toc_rvous_cancel <screen_name> <cookie> <service_uuid>
+```
+
+**Example:**
+
+```
+toc_rvous_cancel toctest2 fUdgNuQ6AAA= 09461343-4C7F-11D1-8222-444553540000
+```
+
+---
+
+## 4. Client → Server Commands (TOC 2)
+
+TOC 2 commands use the `toc2_` prefix and operate on the server-side buddy
+list (feedbag) directly. Changes are automatically saved.
+
+### toc2_send_im
+
+Same syntax as `toc_send_im`.
+
+```
+toc2_send_im <destination_user> <message> [auto]
+```
+
+**Example:**
+
+```
+toc2_send_im toctest2 "Hey there!"
+```
+
+### toc2_send_im_enc
+
+Send an encoded instant message. Supports encoding and language parameters.
+
+```
+toc2_send_im_enc <destination_user> "F" <encoding> <language> <message> [auto]
+```
+
+| Parameter | Description |
+|-----------|-------------|
+| destination_user | Normalized screen name |
+| "F" | Unknown flag, always `F` |
+| encoding | Character encoding (e.g. `A` for ASCII) |
+| language | Language code (e.g. `en`) |
+| message | Quoted, escaped message |
+| auto | Optional `auto` for auto-response |
+
+**Example:**
+
+```
+toc2_send_im_enc toctest2 "F" A en "Hello from TOC2!"
+```
+
+### toc2_set_pdmode
+
+Set the permit/deny privacy mode.
+
+```
+toc2_set_pdmode <mode>
+```
+
+| Mode | Description |
+|------|-------------|
+| 1 | Allow all (default) |
+| 2 | Block all |
+| 3 | Allow permit list only |
+| 4 | Block deny list only |
+| 5 | Allow buddy list only |
+
+**Example:**
+
+```
+toc2_set_pdmode 1
+```
+
+### toc2_new_group
+
+Create a new buddy list group.
+
+```
+toc2_new_group <group_name>
+```
+
+**Example:**
+
+```
+toc2_new_group "Work Friends"
+```
+
+### toc2_del_group
+
+Delete a buddy list group and all buddies within it.
+
+```
+toc2_del_group <group_name>
+```
+
+**Example:**
+
+```
+toc2_del_group "Work Friends"
+```
+
+### toc2_new_buddies
+
+Add buddies to the server-side buddy list using a config-format string. If the
+specified group does not exist, it will be created.
+
+```
+toc2_new_buddies <config_format>
+```
+
+**Config format:** `{g:<group>\nb:<buddy>[:<alias>]\nb:<buddy2>\n}`
+
+Where `\n` is a literal linefeed character (ASCII 10, `0x0A`).
+
+Extended buddy format with alias and note:
+`b:<buddy>:<alias>:::::note`
+
+**Examples:**
+
+Add two buddies to the "Friends" group:
+
+```
+toc2_new_buddies "{g:Friends\nb:toctest2\nb:joe\n}"
+```
+
+Add a buddy with an alias:
+
+```
+toc2_new_buddies "{g:Friends\nb:toctest2:Test User 2\n}"
+```
+
+### toc2_remove_buddy
+
+Remove one or more buddies from a group. The last argument is always the group
+name.
+
+```
+toc2_remove_buddy <screen_name> [<screen_name> ...] <group>
+```
+
+**Examples:**
+
+```
+toc2_remove_buddy toctest2 "Friends"
+toc2_remove_buddy toctest2 joe "Friends"
+```
+
+### toc2_add_permit
+
+Add users to the permit list (feedbag-based).
+
+```
+toc2_add_permit <screen_name> [<screen_name> ...]
+```
+
+**Example:**
+
+```
+toc2_add_permit toctest2 joe
+```
+
+### toc2_remove_permit
+
+Remove users from the permit list.
+
+```
+toc2_remove_permit <screen_name> [<screen_name> ...]
+```
+
+**Example:**
+
+```
+toc2_remove_permit toctest2
+```
+
+### toc2_add_deny
+
+Add users to the deny (block) list (feedbag-based).
+
+```
+toc2_add_deny <screen_name> [<screen_name> ...]
+```
+
+**Example:**
+
+```
+toc2_add_deny spammer
+```
+
+### toc2_remove_deny
+
+Remove users from the deny (block) list.
+
+```
+toc2_remove_deny <screen_name> [<screen_name> ...]
+```
+
+**Example:**
+
+```
+toc2_remove_deny spammer
+```
+
+### toc2_client_event
+
+Send a typing notification to another user.
+
+```
+toc2_client_event <screen_name> <typing_status>
+```
+
+| Status | Description |
+|--------|-------------|
+| 0 | Idle (no activity) |
+| 1 | Text entered (paused typing) |
+| 2 | Currently typing |
+
+**Example:**
+
+```
+toc2_client_event toctest2 2
+```
+
+---
+
+## 5. Server → Client Messages (TOC 1)
+
+Server messages use colons as separators. Messages are NOT null-terminated.
+When parsing, be aware that message content (IMs, chat messages) may contain
+colons — split with a maximum field count appropriate to each command.
+
+### SIGN_ON
+
+Sent after a successful login.
+
+```
+SIGN_ON:<protocol_version>
+```
+
+**Example:**
+
+```
+SIGN_ON:TOC1.0
+```
+
+### CONFIG
+
+The user's saved configuration (buddy list, permit/deny). Only sent for TOC 1
+logins. Config may be empty if none was previously saved.
+
+```
+CONFIG:<config_data>
+```
+
+Config format uses space-separated item types (same format as `toc_set_config`):
+
+```
+CONFIG:m 1
+g Buddies
+b toctest2
+b joe
+d spammer123
+```
+
+Config may be empty (`CONFIG:`) if no configuration has been saved.
+
+### NICK
+
+The user's properly formatted screen name.
+
+```
+NICK:<formatted_screen_name>
+```
+
+**Example:**
+
+```
+NICK:TocTest1
+```
+
+### IM_IN
+
+An incoming instant message.
+
+```
+IM_IN:<source_user>:<auto_response T/F>:<message>
+```
+
+Everything after the third colon is the message (may contain additional
+colons).
+
+**Examples:**
+
+```
+IM_IN:TocTest2:F:Hello there!
+IM_IN:TocTest2:T:I am away from my computer right now.
+IM_IN:TocTest2:F:<b>Bold</b> and <i>italic</i>
+```
+
+### UPDATE_BUDDY
+
+Buddy presence update. Handles arrivals, departures, and status changes.
+
+```
+UPDATE_BUDDY:<screen_name>:<online T/F>:<warning_level>:<signon_time>:<idle_minutes>:<user_class>
+```
+
+| Field | Description |
+|-------|-------------|
+| screen_name | Formatted screen name |
+| online | `T` if online, `F` if offline |
+| warning_level | Warning percentage (0-100) |
+| signon_time | Unix epoch timestamp of signon |
+| idle_minutes | Minutes idle (0 = active) |
+| user_class | 2-3 character class string (see below) |
+
+**User class characters:**
+
+| Position | Char | Meaning |
+|----------|------|---------|
+| uc[0] | `' '` | Normal |
+| uc[0] | `'A'` | On AOL |
+| uc[1] | `' '` | Normal |
+| uc[1] | `'A'` | Admin |
+| uc[1] | `'U'` | Unconfirmed |
+| uc[1] | `'O'` | OSCAR Free (normal AIM user) |
+| uc[1] | `'C'` | Mobile/wireless |
+| uc[2] | `' '` | Available |
+| uc[2] | `'U'` | Unavailable (away) |
+
+**Examples:**
+
+```
+UPDATE_BUDDY:TocTest2:T:0:1711036800:0: O
+UPDATE_BUDDY:TocTest2:T:0:1711036800:0: U
+UPDATE_BUDDY:TocTest2:F:0:0:0:
+UPDATE_BUDDY:TocTest2:T:20:1711036800:5: OU
+```
+
+### ERROR
+
+An error response. May include a screen name or sub-error code as a variable
+argument.
+
+```
+ERROR:<error_code>[:<variable>]
+```
+
+**Example:**
+
+```
+ERROR:901:toctest2
+ERROR:980
+```
+
+See [Error Codes](#7-error-codes) for the full list.
+
+### EVILED
+
+The user has been warned.
+
+```
+EVILED:<new_warning_level>:<eviler_screen_name>
+```
+
+If the warning was anonymous, the eviler field is empty.
+
+**Examples:**
+
+```
+EVILED:20:TocTest2
+EVILED:10:
+```
+
+### CHAT_JOIN
+
+Successfully joined a chat room. The chat room ID is an integer assigned by
+the server — store it for subsequent chat commands.
+
+```
+CHAT_JOIN:<chat_room_id>:<chat_room_name>
+```
+
+**Example:**
+
+```
+CHAT_JOIN:0:My Chat Room
+```
+
+### CHAT_IN
+
+A message received in a chat room.
+
+```
+CHAT_IN:<chat_room_id>:<source_user>:<whisper T/F>:<message>
+```
+
+**Example:**
+
+```
+CHAT_IN:0:TocTest2:F:Hello everyone!
+```
+
+### CHAT_UPDATE_BUDDY
+
+Users have joined or left a chat room. The first message of this type for a
+room contains the initial user list.
+
+```
+CHAT_UPDATE_BUDDY:<chat_room_id>:<inside T/F>:<user1>[:<user2>...]
+```
+
+| Field | Description |
+|-------|-------------|
+| inside | `T` = users arrived, `F` = users departed |
+
+**Examples:**
+
+```
+CHAT_UPDATE_BUDDY:0:T:TocTest1:TocTest2
+CHAT_UPDATE_BUDDY:0:F:TocTest2
+```
+
+### CHAT_INVITE
+
+An invitation to join a chat room.
+
+```
+CHAT_INVITE:<chat_room_name>:<chat_room_id>:<sender>:<message>
+```
+
+**Example:**
+
+```
+CHAT_INVITE:My Chat Room:5:TocTest2:Come join us!
+```
+
+### CHAT_LEFT
+
+Confirmation that you have left a chat room.
+
+```
+CHAT_LEFT:<chat_room_id>
+```
+
+**Example:**
+
+```
+CHAT_LEFT:0
+```
+
+### GOTO_URL
+
+Directs the client to open a URL (for profiles, directory info, search
+results).
+
+```
+GOTO_URL:<window_name>:<url>
+```
+
+**Example:**
+
+```
+GOTO_URL:profile:info?cookie=abc123&user=toctest2&from=toctest1
+```
+
+### DIR_STATUS
+
+Result of a `toc_set_dir` command.
+
+```
+DIR_STATUS:<return_code>
+```
+
+Return code 0 indicates success.
+
+### ADMIN_NICK_STATUS
+
+Result of a `toc_format_nickname` command.
+
+```
+ADMIN_NICK_STATUS:<return_code>
+```
+
+Return code 0 indicates success.
+
+**Example:**
+
+```
+ADMIN_NICK_STATUS:0
+```
+
+### ADMIN_PASSWD_STATUS
+
+Result of a `toc_change_passwd` command.
+
+```
+ADMIN_PASSWD_STATUS:<return_code>
+```
+
+Return code 0 indicates success.
+
+### RVOUS_PROPOSE
+
+Another user proposes a rendezvous (file transfer, etc.).
+
+```
+RVOUS_PROPOSE:<user>:<uuid>:<cookie>:<seq>:<rvous_ip>:<proposer_ip>:<verified_ip>:<port>[:<tlv_tag>:<tlv_value>...]
+```
+
+| Field | Description |
+|-------|-------------|
+| user | Screen name of the proposer |
+| uuid | Service UUID (e.g. file transfer) |
+| cookie | Base64-encoded session cookie |
+| seq | Sequence number |
+| rvous_ip | Rendezvous IP address |
+| proposer_ip | Proposer's IP address |
+| verified_ip | Server-verified IP address |
+| port | Port number for direct connection |
+| tlv pairs | Additional base64-encoded TLV data |
+
+**Example:**
+
+```
+RVOUS_PROPOSE:TocTest2:09461343-4C7F-11D1-8222-444553540000:fUdgNuQ6AAA=:1:192.168.1.5:192.168.1.5:203.0.113.1:5190:10001:ABIAAgAAAAEA
+```
+
+### PAUSE
+
+Server requests the client to pause. A new `SIGN_ON` will follow when the
+server is ready. The buddy list and permit/deny items must be resent,
+followed by `toc_init_done`.
+
+```
+PAUSE
+```
+
+---
+
+## 6. Server → Client Messages (TOC 2)
+
+TOC 2 extends the server messages with richer variants. When logged in via
+`toc2_signon` or `toc2_login`, these messages are sent instead of (or in
+addition to) their TOC 1 equivalents.
+
+### SIGN_ON (TOC 2)
+
+```
+SIGN_ON:TOC2.0
+```
+
+### CONFIG2
+
+Server-side buddy list configuration, sent at login. Fields are separated by
+linefeeds (`\n`, ASCII 10). Colons separate sub-fields within each line.
+
+```
+CONFIG2:<config_lines>
+```
+
+**Config line types:**
+
+| Prefix | Format | Description |
+|--------|--------|-------------|
+| `g:` | `g:<group_name>` | Buddy group |
+| `b:` | `b:<screen_name>[:<alias>][:::::note]` | Buddy entry (with optional alias and note) |
+| `d:` | `d:<screen_name>` | Deny list entry |
+| `p:` | `p:<screen_name>` | Permit list entry |
+| `m:` | `m:<mode>` | Privacy mode (1-5, same as `toc2_set_pdmode`) |
+| `done:` | `done:` | End of config |
+
+**Example:**
+
+```
+CONFIG2:g:Buddies
+b:toctest2:Test User
+b:joe
+d:spammer
+m:1
+done:
+```
+
+### NICK (TOC 2)
+
+Same as TOC 1:
+
+```
+NICK:<formatted_screen_name>
+```
+
+### IM_IN2
+
+Incoming instant message (TOC 2 variant). Adds a whisper field.
+
+```
+IM_IN2:<source_user>:<auto_response T/F>:<whisper T/F>:<message>
+```
+
+**Example:**
+
+```
+IM_IN2:TocTest2:F:F:Hello from TOC2!
+```
+
+### IM_IN_ENC2
+
+Incoming encoded instant message (TOC 2 with `toc2_login`). Includes user
+class and language information.
+
+```
+IM_IN_ENC2:<user>:<auto T/F>:<unknown1>:<unknown2>:<user_class>:<unknown3>:<unknown4>:<language>:<message>
+```
+
+| Field | Description |
+|-------|-------------|
+| user | Sender's screen name |
+| auto | `T` if auto-response, `F` otherwise |
+| unknown1 | Typically `F` |
+| unknown2 | Typically `T` |
+| user_class | Same as UPDATE_BUDDY user class (e.g. ` O `, ` OU`) |
+| unknown3 | Typically `F` |
+| unknown4 | Typically `L` |
+| language | Language code (e.g. `en`) |
+| message | The message content (may contain colons) |
+
+**Example:**
+
+```
+IM_IN_ENC2:TocTest2:F:F:T: O :F:L:en:Hello from TOC2!
+IM_IN_ENC2:TocTest2:T:F:T: OU:F:L:en:I am away right now.
+```
+
+### UPDATE_BUDDY2
+
+Buddy presence update (TOC 2). Same as `UPDATE_BUDDY` with a trailing field.
+
+```
+UPDATE_BUDDY2:<screen_name>:<online T/F>:<warning_level>:<signon_time>:<idle_minutes>:<user_class>:<unknown>
+```
+
+The trailing unknown field is typically empty.
+
+**Examples:**
+
+```
+UPDATE_BUDDY2:TocTest2:T:0:1711036800:0: O :
+UPDATE_BUDDY2:TocTest2:F:0:0:0:   :
+```
+
+### BUDDY_CAPS2
+
+Sent alongside `UPDATE_BUDDY2` to describe a buddy's capabilities.
+
+```
+BUDDY_CAPS2:<screen_name>:<cap1>,<cap2>,...
+```
+
+Capabilities are UUIDs (see [Capabilities](#9-capabilities-uuids)).
+
+**Example:**
+
+```
+BUDDY_CAPS2:TocTest2:748f2420-6287-11d1-8222-444553540000,09461343-4c7f-11d1-8222-444553540000
+```
+
+### CHAT_IN_ENC
+
+Encoded chat message (TOC 2 with `toc2_login`).
+
+```
+CHAT_IN_ENC:<chat_room_id>:<source_user>:<whisper T/F>:<unknown>:<language>:<message>
+```
+
+The unknown field is typically `A`.
+
+**Example:**
+
+```
+CHAT_IN_ENC:0:TocTest2:F:A:en:Hello everyone!
+```
+
+### NEW_BUDDY_REPLY2
+
+Confirmation after adding a buddy via `toc2_new_buddies`.
+
+```
+NEW_BUDDY_REPLY2:<screen_name>:<action>
+```
+
+| Action | Description |
+|--------|-------------|
+| `added` | Buddy was added successfully |
+| `auth` | ICQ authorization request sent |
+
+**Example:**
+
+```
+NEW_BUDDY_REPLY2:toctest2:added
+```
+
+### CLIENT_EVENT2
+
+Typing notification from another user.
+
+```
+CLIENT_EVENT2:<screen_name>:<typing_status>
+```
+
+| Status | Description |
+|--------|-------------|
+| 0 | No activity (stopped typing) |
+| 1 | Text entered (paused) |
+| 2 | Currently typing |
+
+**Example:**
+
+```
+CLIENT_EVENT2:TocTest2:2
+```
+
+### INSERTED2
+
+Dynamic buddy list update — items added from another session.
+
+**Group added:**
+
+```
+INSERTED2:g:<group_name>
+```
+
+**Buddy added:**
+
+```
+INSERTED2:b:<alias>:<screen_name>:<group>
+```
+
+**Deny list entry added:**
+
+```
+INSERTED2:d:<screen_name>
+```
+
+**Permit list entry added:**
+
+```
+INSERTED2:p:<screen_name>
+```
+
+**Examples:**
+
+```
+INSERTED2:g:New Group
+INSERTED2:b:My Friend:toctest2:Buddies
+INSERTED2:b::joe:Buddies
+INSERTED2:d:spammer
+INSERTED2:p:trusteduser
+```
+
+### DELETED2
+
+Dynamic buddy list update — items removed from another session.
+
+**Group deleted:**
+
+```
+DELETED2:g:<group_name>
+```
+
+**Buddy deleted:**
+
+```
+DELETED2:b:<screen_name>:<group>
+```
+
+**Deny list entry removed:**
+
+```
+DELETED2:d:<screen_name>
+```
+
+**Permit list entry removed:**
+
+```
+DELETED2:p:<screen_name>
+```
+
+**Examples:**
+
+```
+DELETED2:g:Old Group
+DELETED2:b:toctest2:Buddies
+DELETED2:d:spammer
+```
+
+### BART2
+
+Buddy icon (Buddy Art) information. Structure is not fully understood.
+
+```
+BART2:<screen_name>:<data>
+```
+
+---
+
+## 7. Error Codes
+
+Errors are sent as `ERROR:<code>[:<variable>]`. The variable argument (if
+present) is typically a screen name or sub-error code.
+
+### General Errors
+
+| Code | Description |
+|------|-------------|
+| 901 | `$1` not currently available |
+| 902 | Warning of `$1` not currently available |
+| 903 | Message dropped, server speed limit exceeded |
+
+### Admin Errors
+
+| Code | Description |
+|------|-------------|
+| 911 | Error validating input |
+| 912 | Invalid account |
+| 913 | Error encountered while processing request |
+| 914 | Service unavailable |
+
+### Chat Errors
+
+| Code | Description |
+|------|-------------|
+| 950 | Chat in `$1` is unavailable |
+
+### IM & Info Errors
+
+| Code | Description |
+|------|-------------|
+| 960 | Sending messages too fast to `$1` |
+| 961 | Missed an IM from `$1` because it was too big |
+| 962 | Missed an IM from `$1` because it was sent too fast |
+
+### Directory Errors
+
+| Code | Description |
+|------|-------------|
+| 970 | Failure |
+| 971 | Too many matches |
+| 972 | Need more qualifiers |
+| 973 | Dir service temporarily unavailable |
+| 974 | Email lookup restricted |
+| 975 | Keyword ignored |
+| 976 | No keywords |
+| 977 | Language not supported |
+| 978 | Country not supported |
+| 979 | Failure unknown `$1` |
+
+### Auth Errors
+
+| Code | Description |
+|------|-------------|
+| 980 | Incorrect nickname or password |
+| 981 | Service temporarily unavailable |
+| 982 | Warning level too high to sign on |
+| 983 | Connecting/disconnecting too frequently (wait 10 minutes) |
+| 989 | Unknown signon error `$1` |
+
+**Sub-error codes for 989:**
+
+| Sub-code | Description |
+|----------|-------------|
+| 0 | Signed on too soon |
+| 7 | Invalid screen name or password |
+| 17 | Suspended account |
+
+---
+
+## 8. Escaping & Encoding Rules
+
+### 8.1 Client → Server (Encoding)
+
+When sending commands to the server, certain characters must be
+**backslash-escaped** whether inside quotes or not:
+
+| Character | Escaped |
+|-----------|---------|
+| `\` | `\\` |
+| `$` | `\$` |
+| `"` | `\"` |
+| `(` | `\(` |
+| `)` | `\)` |
+| `{` | `\{` |
+| `}` | `\}` |
+| `[` | `\[` |
+| `]` | `\]` |
+
+**Example:** To send the message `He said "hello" (wow) for $5`:
+
+```
+toc_send_im toctest2 "He said \"hello\" \(wow\) for \$5"
+```
+
+### 8.2 Screen Name Normalization
+
+When sending screen names to the server, **normalize** them:
+
+1. Convert to lowercase.
+2. Remove all spaces.
+
+Example: `Toc Test 1` → `toctest1`
+
+### 8.3 Server → Client (No Encoding)
+
+Server messages are NOT encoded. They use colons as delimiters. When parsing,
+limit the number of splits to avoid breaking messages that contain colons.
+
+### 8.4 Null Termination
+
+- **Client → Server:** All FLAP DATA payloads MUST end with a null byte
+  (`0x00`). Include it in the FLAP data length.
+- **Server → Client:** FLAP DATA payloads are NOT null-terminated.
+
+---
+
+## 9. Capabilities (UUIDs)
+
+Capabilities indicate which features a client supports. They are set via
+`toc_set_caps` and reported via `BUDDY_CAPS2`.
+
+### Standard Capabilities
+
+| Name | UUID |
+|------|------|
+| Voice Chat | `09461341-4C7F-11D1-8222-444553540000` |
+| File Send | `09461343-4C7F-11D1-8222-444553540000` |
+| Image | `09461345-4C7F-11D1-8222-444553540000` |
+| Buddy Icon | `09461346-4C7F-11D1-8222-444553540000` |
+| Stocks | `09461347-4C7F-11D1-8222-444553540000` |
+| File Get | `09461348-4C7F-11D1-8222-444553540000` |
+| Games | `0946134A-4C7F-11D1-8222-444553540000` |
+| Send Buddy List | `0946134B-4C7F-11D1-8222-444553540000` |
+| AIM/ICQ Interop | `0946134D-4C7F-11D1-8222-444553540000` |
+| Chat | `748F2420-6287-11D1-8222-444553540000` |
+
+### Short Capability Format
+
+Some clients send capabilities as short hex codes (1-4 hex digits). These
+expand to the full OSCAR capability UUID format:
+`0946XXYY-4C7F-11D1-8222-444553540000` where `XXYY` is the short code
+zero-padded to 4 hex digits.
+
+**Example:** Short cap `1343` → `09461343-4C7F-11D1-8222-444553540000` (File Send)
+
+---
+
+## 10. Complete Session Examples
+
+### 10.1 TOC 1 Session (Minimal)
+
+This example shows a complete TOC 1 session: login, add a buddy, send a
+message, and disconnect.
+
+**Wire-level (showing FLAP framing conceptually):**
+
+```
+→ TCP connect to server:9898
+→ "FLAPON\r\n\r\n"
+← [FLAP SIGNON] version=1
+→ [FLAP SIGNON] version=1, TLV(1)="toctest1"
+→ [FLAP DATA]   toc_signon login.oscar.aol.com 5190 toctest1 0x2408105c23001130 english "TIC:Example"
+← [FLAP DATA]   SIGN_ON:TOC1.0
+← [FLAP DATA]   CONFIG:m 1\ng Buddies\nb toctest2\n
+← [FLAP DATA]   NICK:toctest1
+→ [FLAP DATA]   toc_add_buddy toctest2
+→ [FLAP DATA]   toc_init_done
+← [FLAP DATA]   UPDATE_BUDDY:toctest2:T:0:1711036800:0: O
+→ [FLAP DATA]   toc_send_im toctest2 "Hello!"
+→ [FLAP DATA]   toc_set_away "Gone fishing"
+→ [FLAP SIGNOFF]
+```
+
+### 10.2 TOC 2 Session (toc2_login)
+
+```
+→ TCP connect to server:9898
+→ "FLAPON\r\n\r\n"
+← [FLAP SIGNON] version=1
+→ [FLAP SIGNON] version=1, TLV(1)="toctest1"
+→ [FLAP DATA]   toc2_login login.oscar.aol.com 5190 toctest1 0x2408105c23001130 english "TIC:Example" 160 US "" "" 3 0 30303 -kentucky -utf8 103547776
+← [FLAP DATA]   SIGN_ON:TOC2.0
+← [FLAP DATA]   NICK:toctest1
+← [FLAP DATA]   CONFIG2:g:Buddies\nb:toctest2:Test User\nm:1\ndone:\n
+→ [FLAP DATA]   toc_init_done
+← [FLAP DATA]   UPDATE_BUDDY2:toctest2:T:0:1711036800:0: O :
+← [FLAP DATA]   BUDDY_CAPS2:toctest2:748f2420-6287-11d1-8222-444553540000
+→ [FLAP DATA]   toc2_new_buddies "{g:Friends\nb:joe\n}"
+← [FLAP DATA]   NEW_BUDDY_REPLY2:joe:added
+→ [FLAP DATA]   toc2_send_im_enc toctest2 "F" A en "Hello from TOC2!"
+→ [FLAP DATA]   toc2_client_event toctest2 2
+→ [FLAP DATA]   toc2_set_pdmode 1
+→ [FLAP SIGNOFF]
+```
+
+### 10.3 Chat Session
+
+```
+→ toc_chat_join 4 "Retro Chat"
+← CHAT_JOIN:0:Retro Chat
+← CHAT_UPDATE_BUDDY:0:T:toctest1
+→ toc_chat_send 0 "Hello room!"
+← CHAT_IN:0:toctest1:F:Hello room!
+← CHAT_UPDATE_BUDDY:0:T:toctest2
+→ toc_chat_invite 0 "Come chat!" joe
+→ toc_chat_send 0 "Welcome toctest2!"
+← CHAT_IN:0:toctest1:F:Welcome toctest2!
+→ toc_chat_leave 0
+← CHAT_LEFT:0
+```
+
+### 10.4 Python Client Example
+
+A minimal Python client that connects, logs in, sends a message, and reads
+responses:
+
+```python
+import socket
+import struct
+
+
+ROAST = "Tic/Toc"
+
+
+def roast_password(password):
+    result = []
+    for i, ch in enumerate(password):
+        xored = ord(ch) ^ ord(ROAST[i % len(ROAST)])
+        result.append(f"{xored:02x}")
+    return "0x" + "".join(result)
+
+
+def send_flap(sock, frame_type, seq, payload):
+    data = payload.encode("ascii") if isinstance(payload, str) else payload
+    if frame_type == 2:
+        data += b"\x00"  # null-terminate DATA frames
+    header = struct.pack("!BBHH", 0x2A, frame_type, seq, len(data))
+    sock.sendall(header + data)
+    return seq + 1
+
+
+def recv_flap(sock):
+    header = b""
+    while len(header) < 6:
+        chunk = sock.recv(6 - len(header))
+        if not chunk:
+            raise ConnectionError("Connection closed")
+        header += chunk
+    marker, frame_type, seq, length = struct.unpack("!BBHH", header)
+    payload = b""
+    while len(payload) < length:
+        chunk = sock.recv(length - len(payload))
+        if not chunk:
+            raise ConnectionError("Connection closed")
+        payload += chunk
+    return frame_type, seq, payload
+
+
+def normalize(screen_name):
+    return screen_name.lower().replace(" ", "")
+
+
+def escape(text):
+    for ch in r'\$"(){}[]':
+        text = text.replace(ch, "\\" + ch)
+    return text
+
+
+def main():
+    HOST = "127.0.0.1"
+    PORT = 9898
+    USERNAME = "toctest1"
+    PASSWORD = "testpass1"
+
+    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    sock.connect((HOST, PORT))
+    sock.settimeout(5)
+
+    # Step 1: Send FLAPON
+    sock.sendall(b"FLAPON\r\n\r\n")
+
+    # Step 2: Receive server SIGNON frame
+    frame_type, seq, payload = recv_flap(sock)
+    assert frame_type == 1, f"Expected SIGNON frame, got {frame_type}"
+
+    # Step 3: Send client SIGNON frame
+    sn = normalize(USERNAME).encode("ascii")
+    signon_payload = struct.pack("!IHH", 1, 1, len(sn)) + sn
+    seq_out = send_flap(sock, 1, 0, signon_payload)
+
+    # Step 4: Send toc_signon
+    roasted = roast_password(PASSWORD)
+    cmd = f'toc_signon login.oscar.aol.com 5190 {normalize(USERNAME)} {roasted} english "TIC:PythonTOC"'
+    seq_out = send_flap(sock, 2, seq_out, cmd)
+
+    # Step 5: Read responses
+    for _ in range(10):
+        try:
+            frame_type, seq, payload = recv_flap(sock)
+            if frame_type == 2:
+                print(f"← {payload.decode('ascii', errors='replace')}")
+        except socket.timeout:
+            break
+
+    # Step 6: Send init_done
+    seq_out = send_flap(sock, 2, seq_out, "toc_init_done")
+
+    # Step 7: Send a message
+    msg = escape("Hello from Python!")
+    seq_out = send_flap(sock, 2, seq_out, f'toc_send_im toctest2 "{msg}"')
+
+    # Read any remaining responses
+    for _ in range(10):
+        try:
+            frame_type, seq, payload = recv_flap(sock)
+            if frame_type == 2:
+                print(f"← {payload.decode('ascii', errors='replace')}")
+        except socket.timeout:
+            break
+
+    # Disconnect
+    send_flap(sock, 4, seq_out, b"")
+    sock.close()
+
+
+if __name__ == "__main__":
+    main()
+```