Spaces:
Running
Running
Verify Cloudflare runtime compatibility and add read-only relay diagnostics
Browse files- README.md +4 -0
- receiver.py +12 -4
- telegram-relay/worker.mjs +14 -4
README.md
CHANGED
|
@@ -63,6 +63,10 @@ Telegram as before. The Worker holds its own bot token and allows only this
|
|
| 63 |
monitor's five Bot API methods for one numeric chat ID. Both normal delivery and
|
| 64 |
authenticated diagnostics use the configured transport.
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
See `telegram-relay/README.md` for deployment and secret configuration. Verify
|
| 67 |
the relay's read-only getMe, getChat and getChatMember checks before rebuilding
|
| 68 |
the Space, then verify an actual publication arrives after deployment. Neither
|
|
|
|
| 63 |
monitor's five Bot API methods for one numeric chat ID. Both normal delivery and
|
| 64 |
authenticated diagnostics use the configured transport.
|
| 65 |
|
| 66 |
+
When the relay is configured, read-only `/diagnostics` and `/status` also accept
|
| 67 |
+
`Authorization: Bearer TELEGRAM_RELAY_SECRET`. The existing webhook-secret
|
| 68 |
+
routes continue to work. This does not authorize incoming publication webhooks.
|
| 69 |
+
|
| 70 |
See `telegram-relay/README.md` for deployment and secret configuration. Verify
|
| 71 |
the relay's read-only getMe, getChat and getChatMember checks before rebuilding
|
| 72 |
the Space, then verify an actual publication arrives after deployment. Neither
|
receiver.py
CHANGED
|
@@ -28,7 +28,7 @@ from urllib.parse import urlsplit
|
|
| 28 |
from zoneinfo import ZoneInfo
|
| 29 |
|
| 30 |
LOG = logging.getLogger("monitor")
|
| 31 |
-
VERSION = "1.
|
| 32 |
MAX_BYTES = 1024 * 1024
|
| 33 |
AI_CHUNK = 12000
|
| 34 |
DEFAULT_PROMPT = """Ты классификатор публикаций для мониторинга нового внешнего вида
|
|
@@ -331,7 +331,9 @@ def network_error_category(exc):
|
|
| 331 |
|
| 332 |
def request_json(url, payload, headers=None, service="api", timeout=45):
|
| 333 |
request = urllib.request.Request(url, data=json.dumps(payload, ensure_ascii=False).encode(),
|
| 334 |
-
headers={"Content-Type": "application/json",
|
|
|
|
|
|
|
| 335 |
try:
|
| 336 |
with urllib.request.urlopen(request, timeout=timeout) as response:
|
| 337 |
result = json.loads(response.read(2 * MAX_BYTES))
|
|
@@ -596,15 +598,21 @@ def make_server(monitor, host="0.0.0.0", port=7860):
|
|
| 596 |
def authorized(self, prefix):
|
| 597 |
return secrets.compare_digest(urlsplit(self.path).path.encode(), (prefix + monitor.config.secret).encode())
|
| 598 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 599 |
def do_GET(self):
|
| 600 |
if urlsplit(self.path).path == "/live":
|
| 601 |
return self.reply(200, {"status": "alive", "version": VERSION})
|
| 602 |
if urlsplit(self.path).path in ("/", "/health"):
|
| 603 |
health = monitor.health()
|
| 604 |
return self.reply(503 if health["status"] in ("storage_unavailable", "worker_unavailable") else 200, health)
|
| 605 |
-
if self.authorized("/diagnostics/"):
|
| 606 |
return self.reply(200, monitor.clients.diagnose_telegram())
|
| 607 |
-
if self.authorized("/status/") or self.authorized("/samples/")
|
|
|
|
| 608 |
try:
|
| 609 |
return self.reply(200, monitor.status())
|
| 610 |
except Exception:
|
|
|
|
| 28 |
from zoneinfo import ZoneInfo
|
| 29 |
|
| 30 |
LOG = logging.getLogger("monitor")
|
| 31 |
+
VERSION = "1.3-telegram-relay"
|
| 32 |
MAX_BYTES = 1024 * 1024
|
| 33 |
AI_CHUNK = 12000
|
| 34 |
DEFAULT_PROMPT = """Ты классификатор публикаций для мониторинга нового внешнего вида
|
|
|
|
| 331 |
|
| 332 |
def request_json(url, payload, headers=None, service="api", timeout=45):
|
| 333 |
request = urllib.request.Request(url, data=json.dumps(payload, ensure_ascii=False).encode(),
|
| 334 |
+
headers={"Content-Type": "application/json", "Accept": "application/json",
|
| 335 |
+
"User-Agent": "MedialogiaMonitor/" + VERSION,
|
| 336 |
+
**(headers or {})}, method="POST")
|
| 337 |
try:
|
| 338 |
with urllib.request.urlopen(request, timeout=timeout) as response:
|
| 339 |
result = json.loads(response.read(2 * MAX_BYTES))
|
|
|
|
| 598 |
def authorized(self, prefix):
|
| 599 |
return secrets.compare_digest(urlsplit(self.path).path.encode(), (prefix + monitor.config.secret).encode())
|
| 600 |
|
| 601 |
+
def relay_authorized(self):
|
| 602 |
+
secret = monitor.config.telegram_relay_secret
|
| 603 |
+
return bool(secret) and secrets.compare_digest(
|
| 604 |
+
self.headers.get("Authorization", "").encode(), ("Bearer " + secret).encode())
|
| 605 |
+
|
| 606 |
def do_GET(self):
|
| 607 |
if urlsplit(self.path).path == "/live":
|
| 608 |
return self.reply(200, {"status": "alive", "version": VERSION})
|
| 609 |
if urlsplit(self.path).path in ("/", "/health"):
|
| 610 |
health = monitor.health()
|
| 611 |
return self.reply(503 if health["status"] in ("storage_unavailable", "worker_unavailable") else 200, health)
|
| 612 |
+
if self.authorized("/diagnostics/") or (urlsplit(self.path).path == "/diagnostics" and self.relay_authorized()):
|
| 613 |
return self.reply(200, monitor.clients.diagnose_telegram())
|
| 614 |
+
if (self.authorized("/status/") or self.authorized("/samples/")
|
| 615 |
+
or (urlsplit(self.path).path == "/status" and self.relay_authorized())):
|
| 616 |
try:
|
| 617 |
return self.reply(200, monitor.status())
|
| 618 |
except Exception:
|
telegram-relay/worker.mjs
CHANGED
|
@@ -83,19 +83,29 @@ export default {
|
|
| 83 |
method: "POST",
|
| 84 |
headers: { "Content-Type": "application/json" },
|
| 85 |
body: JSON.stringify(input.payload),
|
| 86 |
-
redirect: "
|
| 87 |
signal: AbortSignal.timeout(8000),
|
| 88 |
},
|
| 89 |
);
|
|
|
|
| 90 |
const body = await response.text();
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
if (!data || typeof data.ok !== "boolean") return reply(502, "telegram_invalid_response");
|
| 93 |
return new Response(body, {
|
| 94 |
status: response.status,
|
| 95 |
headers: { "Content-Type": "application/json", "Cache-Control": "no-store" },
|
| 96 |
});
|
| 97 |
-
} catch {
|
| 98 |
-
return reply(502, "
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
}
|
| 100 |
},
|
| 101 |
};
|
|
|
|
| 83 |
method: "POST",
|
| 84 |
headers: { "Content-Type": "application/json" },
|
| 85 |
body: JSON.stringify(input.payload),
|
| 86 |
+
redirect: "manual",
|
| 87 |
signal: AbortSignal.timeout(8000),
|
| 88 |
},
|
| 89 |
);
|
| 90 |
+
if (response.status >= 300 && response.status < 400) return reply(502, "telegram_unexpected_redirect");
|
| 91 |
const body = await response.text();
|
| 92 |
+
let data;
|
| 93 |
+
try {
|
| 94 |
+
data = JSON.parse(body);
|
| 95 |
+
} catch {
|
| 96 |
+
return reply(502, "telegram_upstream_non_json_http_" + response.status);
|
| 97 |
+
}
|
| 98 |
if (!data || typeof data.ok !== "boolean") return reply(502, "telegram_invalid_response");
|
| 99 |
return new Response(body, {
|
| 100 |
status: response.status,
|
| 101 |
headers: { "Content-Type": "application/json", "Cache-Control": "no-store" },
|
| 102 |
});
|
| 103 |
+
} catch (error) {
|
| 104 |
+
if (typeof AbortSignal.timeout !== "function") return reply(502, "telegram_timeout_api_unavailable");
|
| 105 |
+
if (String(error.message).includes("Network connection lost")) return reply(502, "telegram_network_connection_lost");
|
| 106 |
+
if (String(error.message).includes("resolve")) return reply(502, "telegram_dns_error");
|
| 107 |
+
const category = ["TimeoutError", "AbortError", "TypeError"].includes(error.name) ? error.name : "unknown";
|
| 108 |
+
return reply(502, "telegram_upstream_" + category);
|
| 109 |
}
|
| 110 |
},
|
| 111 |
};
|