telethon-webproxy 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fiksofficial
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,171 @@
1
+ Metadata-Version: 2.4
2
+ Name: telethon-webproxy
3
+ Version: 0.1.0
4
+ Summary: Telegram WEB Proxy connector for Telethon (v1 & v2)
5
+ Author: fiksofficial
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/fiksofficial/telethon-webproxy
8
+ Project-URL: Issues, https://github.com/fiksofficial/telethon-webproxy/issues
9
+ Keywords: telegram,telethon,proxy,mtproto,web-proxy
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Communications :: Chat
20
+ Classifier: Topic :: Internet
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: websockets>=12.0
25
+ Requires-Dist: aiohttp>=3.9
26
+ Requires-Dist: cryptography>=39.0
27
+ Provides-Extra: telethon-v1
28
+ Requires-Dist: telethon>=1.28; extra == "telethon-v1"
29
+ Provides-Extra: telethon-v2
30
+ Requires-Dist: telethon>=2.0; extra == "telethon-v2"
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest; extra == "dev"
33
+ Requires-Dist: pytest-asyncio; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # telethon-webproxy
37
+
38
+ **Telegram WEB Proxy connector for Telethon (v1 & v2).**
39
+
40
+ Позволяет подключать [Telethon](https://codeberg.org/Lonami/Telethon) к Telegram через новый тип прокси — **WEB Proxy** (`tdesktop-web-proxy-bridge-v1`), появившийся в Telegram Desktop 7.1.
41
+
42
+ > [!IMPORTANT]
43
+ > Библиотека общается с relay-сервером напрямую по WebSocket.
44
+ > **Браузер не нужен.** Потребление памяти — несколько сотен КБ на соединение.
45
+
46
+ ## Установка
47
+
48
+ ```bash
49
+ pip install telethon-webproxy
50
+ ```
51
+
52
+ Или с указанием версии Telethon:
53
+
54
+ ```bash
55
+ pip install "telethon-webproxy[telethon-v1]" # Telethon 1.x
56
+ pip install "telethon-webproxy[telethon-v2]" # Telethon 2.x
57
+ ```
58
+
59
+ ## Быстрый старт
60
+
61
+ ### Telethon v1
62
+
63
+ ```python
64
+ from telethon import TelegramClient
65
+ from telethon_webproxy import ConnectionWebProxy
66
+
67
+ client = TelegramClient(
68
+ "session",
69
+ api_id,
70
+ api_hash,
71
+ connection=ConnectionWebProxy,
72
+ # Третий элемент кортежа — словарь с опциями (опционально)
73
+ proxy=("proxy.example.com", "dd00...", {"mode": "websocket-lanes", "reconnect": True}),
74
+ )
75
+
76
+ async def main():
77
+ await client.start()
78
+ me = await client.get_me()
79
+ print(me.first_name)
80
+
81
+ import asyncio
82
+ asyncio.run(main())
83
+ ```
84
+
85
+ ### Telethon v2
86
+
87
+ ```python
88
+ from telethon import Client
89
+ from telethon_webproxy import make_web_proxy_connector
90
+
91
+ connector = make_web_proxy_connector(
92
+ host="proxy.example.com",
93
+ secret_hex="dd00...",
94
+ mode="websocket-lanes",
95
+ reconnect=True,
96
+ )
97
+
98
+ client = Client("session", api_id, api_hash, connector=connector)
99
+ ```
100
+
101
+ ### Автоопределение версии
102
+
103
+ ```python
104
+ from telethon_webproxy import WebProxyConnector
105
+
106
+ # WebProxyConnector — это ConnectionWebProxy для Telethon v1
107
+ # или make_web_proxy_connector для Telethon v2.
108
+ # Определяется автоматически при импорте.
109
+ ```
110
+
111
+ ## Как это работает
112
+
113
+ ```
114
+ ┌──────────────┐ WSS / HTTPS ┌───────────────┐ TCP ┌──────────────┐
115
+ │ Ваш скрипт │ ◄──────────────────► │ tproxy-server│ ◄───────────────► │ Telegram │
116
+ │ (Telethon) │ Фреймы протокола │ (relay) │ MTProto │ DC │
117
+ └──────────────┘ WEB Proxy v1 └───────────────┘ └──────────────┘
118
+ ```
119
+
120
+ 1. Библиотека вычисляет `capability` — HMAC-SHA256 от секрета и домена.
121
+ 2. Запрашивает bridge-страницу (`GET /?bridge=<cap>`) и извлекает bootstrap-токен.
122
+ 3. Создает сессию (`POST /api/v1/session`) и получает session-токен.
123
+ 4. Открывает выбранный транспорт (WSS мультиплекс, WSS на каждый поток, или HTTP long-polling).
124
+ 5. Мультиплексирует MTProto-потоки через фреймы OPEN/DATA/CLOSE/WINDOW.
125
+
126
+ ## Параметры прокси
127
+
128
+ | Параметр | Описание |
129
+ |:---------|:---------|
130
+ | `host` | Доменное имя WEB-прокси сервера (например `proxy.example.com`) |
131
+ | `secret_hex` | Hex-строка секрета MTProxy. Если не начинается с `dd`, библиотека добавит его. |
132
+ | `mode` | Режим работы: `websocket` (по умолч.), `websocket-lanes`, `https`. |
133
+ | `reconnect`| Автоматическое переподключение при обрыве сети (по умолч. `True`). |
134
+
135
+ ## Низкоуровневый API
136
+
137
+ Если вам не нужна интеграция с Telethon, используйте `WebSocketCarrier` напрямую:
138
+
139
+ ```python
140
+ import asyncio
141
+ from telethon_webproxy import WebSocketCarrier
142
+
143
+ async def main():
144
+ carrier = WebSocketCarrier("proxy.example.com", "dd00…")
145
+ await carrier.connect()
146
+
147
+ stream_id = await carrier.open_stream()
148
+ await carrier.send_data(stream_id, b"\\x00\\x00\\x00\\x00...") # raw MTProto
149
+ response = await carrier.recv_data(stream_id)
150
+
151
+ await carrier.close_stream(stream_id)
152
+ await carrier.disconnect()
153
+
154
+ asyncio.run(main())
155
+ ```
156
+
157
+ ## Поддерживаемые серверы
158
+
159
+ - [telegramdesktop/tproxy-server](https://github.com/telegramdesktop/tproxy-server) — эталонная реализация на Go (от команды Telegram)
160
+ - [sleep3r/mtproto.zig](https://github.com/sleep3r/mtproto.zig) — высокопроизводительная реализация на Zig
161
+
162
+ ## Тестирование
163
+
164
+ ```bash
165
+ pip install -e ".[dev]"
166
+ pytest tests/
167
+ ```
168
+
169
+ ## Лицензия
170
+
171
+ MIT
@@ -0,0 +1,136 @@
1
+ # telethon-webproxy
2
+
3
+ **Telegram WEB Proxy connector for Telethon (v1 & v2).**
4
+
5
+ Позволяет подключать [Telethon](https://codeberg.org/Lonami/Telethon) к Telegram через новый тип прокси — **WEB Proxy** (`tdesktop-web-proxy-bridge-v1`), появившийся в Telegram Desktop 7.1.
6
+
7
+ > [!IMPORTANT]
8
+ > Библиотека общается с relay-сервером напрямую по WebSocket.
9
+ > **Браузер не нужен.** Потребление памяти — несколько сотен КБ на соединение.
10
+
11
+ ## Установка
12
+
13
+ ```bash
14
+ pip install telethon-webproxy
15
+ ```
16
+
17
+ Или с указанием версии Telethon:
18
+
19
+ ```bash
20
+ pip install "telethon-webproxy[telethon-v1]" # Telethon 1.x
21
+ pip install "telethon-webproxy[telethon-v2]" # Telethon 2.x
22
+ ```
23
+
24
+ ## Быстрый старт
25
+
26
+ ### Telethon v1
27
+
28
+ ```python
29
+ from telethon import TelegramClient
30
+ from telethon_webproxy import ConnectionWebProxy
31
+
32
+ client = TelegramClient(
33
+ "session",
34
+ api_id,
35
+ api_hash,
36
+ connection=ConnectionWebProxy,
37
+ # Третий элемент кортежа — словарь с опциями (опционально)
38
+ proxy=("proxy.example.com", "dd00...", {"mode": "websocket-lanes", "reconnect": True}),
39
+ )
40
+
41
+ async def main():
42
+ await client.start()
43
+ me = await client.get_me()
44
+ print(me.first_name)
45
+
46
+ import asyncio
47
+ asyncio.run(main())
48
+ ```
49
+
50
+ ### Telethon v2
51
+
52
+ ```python
53
+ from telethon import Client
54
+ from telethon_webproxy import make_web_proxy_connector
55
+
56
+ connector = make_web_proxy_connector(
57
+ host="proxy.example.com",
58
+ secret_hex="dd00...",
59
+ mode="websocket-lanes",
60
+ reconnect=True,
61
+ )
62
+
63
+ client = Client("session", api_id, api_hash, connector=connector)
64
+ ```
65
+
66
+ ### Автоопределение версии
67
+
68
+ ```python
69
+ from telethon_webproxy import WebProxyConnector
70
+
71
+ # WebProxyConnector — это ConnectionWebProxy для Telethon v1
72
+ # или make_web_proxy_connector для Telethon v2.
73
+ # Определяется автоматически при импорте.
74
+ ```
75
+
76
+ ## Как это работает
77
+
78
+ ```
79
+ ┌──────────────┐ WSS / HTTPS ┌───────────────┐ TCP ┌──────────────┐
80
+ │ Ваш скрипт │ ◄──────────────────► │ tproxy-server│ ◄───────────────► │ Telegram │
81
+ │ (Telethon) │ Фреймы протокола │ (relay) │ MTProto │ DC │
82
+ └──────────────┘ WEB Proxy v1 └───────────────┘ └──────────────┘
83
+ ```
84
+
85
+ 1. Библиотека вычисляет `capability` — HMAC-SHA256 от секрета и домена.
86
+ 2. Запрашивает bridge-страницу (`GET /?bridge=<cap>`) и извлекает bootstrap-токен.
87
+ 3. Создает сессию (`POST /api/v1/session`) и получает session-токен.
88
+ 4. Открывает выбранный транспорт (WSS мультиплекс, WSS на каждый поток, или HTTP long-polling).
89
+ 5. Мультиплексирует MTProto-потоки через фреймы OPEN/DATA/CLOSE/WINDOW.
90
+
91
+ ## Параметры прокси
92
+
93
+ | Параметр | Описание |
94
+ |:---------|:---------|
95
+ | `host` | Доменное имя WEB-прокси сервера (например `proxy.example.com`) |
96
+ | `secret_hex` | Hex-строка секрета MTProxy. Если не начинается с `dd`, библиотека добавит его. |
97
+ | `mode` | Режим работы: `websocket` (по умолч.), `websocket-lanes`, `https`. |
98
+ | `reconnect`| Автоматическое переподключение при обрыве сети (по умолч. `True`). |
99
+
100
+ ## Низкоуровневый API
101
+
102
+ Если вам не нужна интеграция с Telethon, используйте `WebSocketCarrier` напрямую:
103
+
104
+ ```python
105
+ import asyncio
106
+ from telethon_webproxy import WebSocketCarrier
107
+
108
+ async def main():
109
+ carrier = WebSocketCarrier("proxy.example.com", "dd00…")
110
+ await carrier.connect()
111
+
112
+ stream_id = await carrier.open_stream()
113
+ await carrier.send_data(stream_id, b"\\x00\\x00\\x00\\x00...") # raw MTProto
114
+ response = await carrier.recv_data(stream_id)
115
+
116
+ await carrier.close_stream(stream_id)
117
+ await carrier.disconnect()
118
+
119
+ asyncio.run(main())
120
+ ```
121
+
122
+ ## Поддерживаемые серверы
123
+
124
+ - [telegramdesktop/tproxy-server](https://github.com/telegramdesktop/tproxy-server) — эталонная реализация на Go (от команды Telegram)
125
+ - [sleep3r/mtproto.zig](https://github.com/sleep3r/mtproto.zig) — высокопроизводительная реализация на Zig
126
+
127
+ ## Тестирование
128
+
129
+ ```bash
130
+ pip install -e ".[dev]"
131
+ pytest tests/
132
+ ```
133
+
134
+ ## Лицензия
135
+
136
+ MIT
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "telethon-webproxy"
7
+ version = "0.1.0"
8
+ authors = [{name = "fiksofficial"}]
9
+ description = "Telegram WEB Proxy connector for Telethon (v1 & v2)"
10
+ readme = "README.md"
11
+ requires-python = ">=3.9"
12
+ license = {text = "MIT"}
13
+ keywords = ["telegram", "telethon", "proxy", "mtproto", "web-proxy"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Communications :: Chat",
25
+ "Topic :: Internet",
26
+ ]
27
+ dependencies = [
28
+ "websockets>=12.0",
29
+ "aiohttp>=3.9",
30
+ "cryptography>=39.0",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ telethon-v1 = ["telethon>=1.28"]
35
+ telethon-v2 = ["telethon>=2.0"]
36
+ dev = ["pytest", "pytest-asyncio"]
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/fiksofficial/telethon-webproxy"
40
+ Issues = "https://github.com/fiksofficial/telethon-webproxy/issues"
41
+
42
+ [tool.setuptools.packages.find]
43
+ include = ["telethon_webproxy*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,78 @@
1
+ """
2
+ telethon-webproxy — Telegram WEB Proxy connector for Telethon.
3
+
4
+ Supports all four carrier modes:
5
+ • ``websocket`` — single multiplexed WebSocket (default)
6
+ • ``websocket-lanes`` — one WebSocket per stream (best isolation)
7
+ • ``https`` — HTTP long-polling (widest compatibility)
8
+ • ``https-lanes`` — per-stream HTTP long-polling
9
+
10
+ Auto-reconnect is available via :class:`ReconnectingCarrier`.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ __version__ = "0.2.0"
16
+
17
+ # ── Protocol core ─────────────────────────────────────────────────────────────
18
+ from .protocol import (
19
+ FrameType,
20
+ Frame,
21
+ compute_capability,
22
+ compute_capability_from_hex,
23
+ encode_frame,
24
+ encode_hello,
25
+ parse_frames,
26
+ )
27
+
28
+ # ── Carrier base ──────────────────────────────────────────────────────────────
29
+ from .carrier_base import (
30
+ BaseCarrier,
31
+ CarrierError,
32
+ HandshakeError,
33
+ StreamClosedError,
34
+ RelayByeError,
35
+ )
36
+
37
+ # ── Carrier implementations ──────────────────────────────────────────────────
38
+ from .carrier import WebSocketCarrier
39
+ from .carrier_https import HTTPSCarrier
40
+ from .carrier_lanes import WebSocketLanesCarrier
41
+ from .reconnect import ReconnectingCarrier
42
+
43
+ # ── Telethon connectors ──────────────────────────────────────────────────────
44
+ from .connector_v1 import ConnectionWebProxy
45
+ from .connector_v2 import make_web_proxy_connector, WebProxyStream
46
+
47
+ # ── Version auto-detect ──────────────────────────────────────────────────────
48
+ _telethon_major: int | None = None
49
+
50
+ try:
51
+ import importlib.metadata as _meta
52
+ _tv = _meta.version("telethon")
53
+ _telethon_major = int(_tv.split(".")[0])
54
+ except Exception:
55
+ pass
56
+
57
+ if _telethon_major is not None and _telethon_major >= 2:
58
+ WebProxyConnector = make_web_proxy_connector
59
+ else:
60
+ WebProxyConnector = ConnectionWebProxy
61
+
62
+ __all__ = [
63
+ # Protocol
64
+ "FrameType", "Frame",
65
+ "compute_capability", "compute_capability_from_hex",
66
+ "encode_frame", "encode_hello", "parse_frames",
67
+ # Carrier base
68
+ "BaseCarrier", "CarrierError", "HandshakeError",
69
+ "StreamClosedError", "RelayByeError",
70
+ # Carriers
71
+ "WebSocketCarrier", "HTTPSCarrier",
72
+ "WebSocketLanesCarrier", "ReconnectingCarrier",
73
+ # Telethon connectors
74
+ "ConnectionWebProxy", "make_web_proxy_connector",
75
+ "WebProxyStream", "WebProxyConnector",
76
+ # Meta
77
+ "__version__",
78
+ ]
@@ -0,0 +1,141 @@
1
+ """
2
+ Multiplexed WebSocket carrier (``websocket`` carrier mode).
3
+
4
+ Reference: PROTOCOL.md §Multiplexed WebSocket
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import logging
11
+ import ssl
12
+ from typing import Optional
13
+
14
+ import aiohttp
15
+
16
+ from .carrier_base import BaseCarrier, StreamClosedError
17
+ from .protocol import (
18
+ DATA_CHUNK,
19
+ WS_SUBPROTOCOL_PREFIX,
20
+ WS_UPGRADE_PATH,
21
+ FrameType,
22
+ encode_frame,
23
+ parse_frames,
24
+ )
25
+
26
+ log = logging.getLogger(__name__)
27
+
28
+
29
+ class WebSocketCarrier(BaseCarrier):
30
+ """Single multiplexed WebSocket carrier.
31
+
32
+ All streams share one WSS connection. A WebSocket loss closes the
33
+ entire relay session (per the spec).
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ host: str,
39
+ secret_hex: str,
40
+ *,
41
+ ssl_context: Optional[ssl.SSLContext] = None,
42
+ ) -> None:
43
+ super().__init__(host, secret_hex, ssl_context=ssl_context)
44
+ self._ws: Optional[aiohttp.ClientWebSocketResponse] = None
45
+ self._reader_task: Optional[asyncio.Task] = None
46
+
47
+ # ── Transport lifecycle ───────────────────────────────────────────────
48
+
49
+ async def _start_transport(self) -> None:
50
+ si = self._session_info
51
+ subprotocol = WS_SUBPROTOCOL_PREFIX + si.token
52
+ self._ws = await self._http.ws_connect(
53
+ f"wss://{si.host}{WS_UPGRADE_PATH}",
54
+ protocols=[subprotocol],
55
+ ssl=self._ssl,
56
+ max_msg_size=4 * 1024 * 1024,
57
+ origin=f"https://{si.host}",
58
+ )
59
+ self._reader_task = asyncio.get_event_loop().create_task(
60
+ self._reader_loop()
61
+ )
62
+ log.info("WebSocket carrier connected to %s", si.host)
63
+
64
+ async def _stop_transport(self) -> None:
65
+ if self._reader_task:
66
+ self._reader_task.cancel()
67
+ try:
68
+ await self._reader_task
69
+ except (asyncio.CancelledError, Exception):
70
+ pass
71
+ self._reader_task = None
72
+ if self._ws and not self._ws.closed:
73
+ await self._ws.close()
74
+ self._ws = None
75
+
76
+ # ── Stream operations ─────────────────────────────────────────────────
77
+
78
+ async def open_stream(self) -> int:
79
+ sid = self._alloc_stream()
80
+ await self._ws_send(encode_frame(FrameType.OPEN, sid))
81
+ return sid
82
+
83
+ async def close_stream(self, stream_id: int) -> None:
84
+ if stream_id in self._open_streams:
85
+ self._open_streams.discard(stream_id)
86
+ await self._ws_send(encode_frame(FrameType.CLOSE, stream_id))
87
+
88
+ async def send_data(self, stream_id: int, data: bytes) -> None:
89
+ offset = 0
90
+ while offset < len(data):
91
+ chunk_size = min(DATA_CHUNK, len(data) - offset)
92
+ await self._wait_send_window(stream_id, chunk_size)
93
+ chunk = data[offset : offset + chunk_size]
94
+ self._send_windows[stream_id] -= len(chunk)
95
+ await self._ws_send(encode_frame(FrameType.DATA, stream_id, chunk))
96
+ offset += chunk_size
97
+
98
+ async def _send_control(self, frame_bytes: bytes) -> None:
99
+ await self._ws_send(frame_bytes)
100
+
101
+ # ── Internals ─────────────────────────────────────────────────────────
102
+
103
+ async def _ws_send(self, data: bytes) -> None:
104
+ if self._ws and not self._ws.closed:
105
+ await self._ws.send_bytes(data)
106
+
107
+ async def _reader_loop(self) -> None:
108
+ try:
109
+ async for msg in self._ws:
110
+ if msg.type == aiohttp.WSMsgType.BINARY:
111
+ await self._on_batch(msg.data)
112
+ elif msg.type == aiohttp.WSMsgType.PING:
113
+ await self._ws.pong(msg.data)
114
+ elif msg.type in (
115
+ aiohttp.WSMsgType.CLOSE,
116
+ aiohttp.WSMsgType.CLOSING,
117
+ aiohttp.WSMsgType.CLOSED,
118
+ aiohttp.WSMsgType.ERROR,
119
+ ):
120
+ break
121
+ except asyncio.CancelledError:
122
+ return
123
+ except Exception as exc:
124
+ log.warning("WebSocket reader error: %s", exc)
125
+ finally:
126
+ if self._connected:
127
+ await self.disconnect()
128
+
129
+ async def _on_batch(self, data: bytes) -> None:
130
+ try:
131
+ frames = parse_frames(data)
132
+ except ValueError as exc:
133
+ log.warning("Malformed relay batch: %s", exc)
134
+ return
135
+ for frame in frames:
136
+ pong = self._dispatch_frame(frame.type, frame.stream_id, frame.payload)
137
+ if pong:
138
+ await self._ws_send(pong)
139
+ if frame.type == FrameType.BYE:
140
+ await self.disconnect()
141
+ return