Selaa lähdekoodia

document toc 1/2 protocols

Mike 3 kuukautta sitten
vanhempi
commit
1c3fe1740b
2 muutettua tiedostoa jossa 1829 lisäystä ja 1 poistoa
  1. 2 1
      README.md
  2. 1827 0
      docs/TOC.md

+ 2 - 1
README.md

@@ -32,7 +32,8 @@ The following features are supported:
 - [x] Privacy (allow or block specific users)
 - [x] Warning
 - [x] User Directory Search
-- [x] TOC Protocol Clients: Quick Buddy, gaim, [TiK](./docs/CLIENT_TIK.md)
+- [x] TOC1 Protocol Clients: Quick Buddy, gaim, [TiK](./docs/CLIENT_TIK.md)
+- [x] TOC2 Protocol Clients: [vAIM](https://www.onlyup.net/vaim/index.html), Miranda ~v0.4.0.3, iEM 1.0.1
 - [x] File Sharing
     - LAN Only: Direct Connect, Get File
     - Lan/Internet: [Send File](./docs/RENDEZVOUS.md)

+ 1827 - 0
docs/TOC.md

@@ -0,0 +1,1827 @@
+# TOC Protocol Specification
+
+Version: TOC 1.0 / TOC 2.0
+
+This document is a 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 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).
+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      | User 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()
+```