Spaces:
Running
Running
Upload 4 files
Browse files
app.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import os
|
|
|
|
| 2 |
import json
|
| 3 |
|
| 4 |
from fastapi import FastAPI, Request
|
|
@@ -10,13 +11,61 @@ import httpx
|
|
| 10 |
UPSTREAM = f"http://127.0.0.1:{os.environ.get('TELEGRAM_UPSTREAM_PORT', '8081')}"
|
| 11 |
app = FastAPI()
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
async def root():
|
| 15 |
return {"ok": True, "hint": "use /tg/ prefix, e.g. /tg/bot<TOKEN>/getMe"}
|
| 16 |
|
| 17 |
@app.api_route("/tg/{path:path}", methods=["GET","POST","PUT","PATCH","DELETE","HEAD","OPTIONS"])
|
| 18 |
async def proxy(path: str, request: Request):
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
params = list(request.query_params.multi_items())
|
| 22 |
|
|
@@ -30,8 +79,8 @@ async def proxy(path: str, request: Request):
|
|
| 30 |
# 关键修复:
|
| 31 |
# - Bot API 的 /bot... 响应是小 JSON(sendDocument/getFile/getMe),直接“读全量再返回”,避免流式上下文提前关闭导致空响应。
|
| 32 |
# - Bot API 的 /file/... 响应是大文件,才需要流式透传。
|
| 33 |
-
is_file_download =
|
| 34 |
-
is_get_file = "/getFile" in ("/" +
|
| 35 |
|
| 36 |
passthrough_allow = {
|
| 37 |
"content-type",
|
|
@@ -69,43 +118,19 @@ async def proxy(path: str, request: Request):
|
|
| 69 |
if isinstance(payload, dict) and payload.get("ok") is True and isinstance(payload.get("result"), dict):
|
| 70 |
result = payload.get("result") or {}
|
| 71 |
fp = result.get("file_path")
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
raw_fp = raw_fp[idx + len(marker) :]
|
| 86 |
-
elif raw_fp.startswith(f"{token_enc}/"):
|
| 87 |
-
raw_fp = raw_fp[len(token_enc) + 1 :]
|
| 88 |
-
|
| 89 |
-
# 最终只保留常见的相对目录开头(documents/photos/videos/voice/audio/animations/stickers 等)
|
| 90 |
-
for prefix in (
|
| 91 |
-
"documents/",
|
| 92 |
-
"photos/",
|
| 93 |
-
"videos/",
|
| 94 |
-
"video_notes/",
|
| 95 |
-
"voice/",
|
| 96 |
-
"audio/",
|
| 97 |
-
"animations/",
|
| 98 |
-
"stickers/",
|
| 99 |
-
"profile_photos/",
|
| 100 |
-
"thumbnails/",
|
| 101 |
-
):
|
| 102 |
-
if raw_fp.startswith(prefix):
|
| 103 |
-
result["file_path"] = raw_fp
|
| 104 |
-
payload["result"] = result
|
| 105 |
-
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
| 106 |
-
# 确保 JSON 的 content-type 不被冲掉
|
| 107 |
-
resp_headers.setdefault("content-type", "application/json")
|
| 108 |
-
return Response(content=body, status_code=r.status_code, headers=resp_headers)
|
| 109 |
except Exception:
|
| 110 |
pass
|
| 111 |
|
|
|
|
| 1 |
import os
|
| 2 |
+
import os
|
| 3 |
import json
|
| 4 |
|
| 5 |
from fastapi import FastAPI, Request
|
|
|
|
| 11 |
UPSTREAM = f"http://127.0.0.1:{os.environ.get('TELEGRAM_UPSTREAM_PORT', '8081')}"
|
| 12 |
app = FastAPI()
|
| 13 |
|
| 14 |
+
def _normalize_bot_api_file_path(raw_fp: str | None, token_enc: str | None) -> str:
|
| 15 |
+
if not raw_fp:
|
| 16 |
+
return ""
|
| 17 |
+
|
| 18 |
+
fp = str(raw_fp).replace("\\", "/").lstrip("/")
|
| 19 |
+
|
| 20 |
+
# 有些 Local Bot API Server 会把 token 段也带进 file_path,先把它裁掉
|
| 21 |
+
if token_enc:
|
| 22 |
+
marker = f"/{token_enc}/"
|
| 23 |
+
idx = fp.find(marker)
|
| 24 |
+
if idx >= 0:
|
| 25 |
+
fp = fp[idx + len(marker) :]
|
| 26 |
+
elif fp.startswith(f"{token_enc}/"):
|
| 27 |
+
fp = fp[len(token_enc) + 1 :]
|
| 28 |
+
|
| 29 |
+
parts = [p for p in fp.split("/") if p]
|
| 30 |
+
roots = {
|
| 31 |
+
"documents",
|
| 32 |
+
"photos",
|
| 33 |
+
"videos",
|
| 34 |
+
"video_notes",
|
| 35 |
+
"voice",
|
| 36 |
+
"audio",
|
| 37 |
+
"animation",
|
| 38 |
+
"animations",
|
| 39 |
+
"stickers",
|
| 40 |
+
"files",
|
| 41 |
+
"thumbnails",
|
| 42 |
+
"profile_photos",
|
| 43 |
+
}
|
| 44 |
+
for i, p in enumerate(parts):
|
| 45 |
+
if p in roots:
|
| 46 |
+
return "/".join(parts[i:])
|
| 47 |
+
return fp
|
| 48 |
+
|
| 49 |
+
@app.get("/")
|
| 50 |
async def root():
|
| 51 |
return {"ok": True, "hint": "use /tg/ prefix, e.g. /tg/bot<TOKEN>/getMe"}
|
| 52 |
|
| 53 |
@app.api_route("/tg/{path:path}", methods=["GET","POST","PUT","PATCH","DELETE","HEAD","OPTIONS"])
|
| 54 |
async def proxy(path: str, request: Request):
|
| 55 |
+
# 兜底:如果下游请求把 file_path 带成了“工作目录前缀”,这里直接改写成相对路径再转发给 telegram-bot-api
|
| 56 |
+
path_for_upstream = path.lstrip("/")
|
| 57 |
+
if path_for_upstream.startswith("file/"):
|
| 58 |
+
# 形如:file/bot<TOKEN>/<file_path>
|
| 59 |
+
# 我们只需要把 <file_path> 规范化成 documents/... 这种相对路径
|
| 60 |
+
rest = path_for_upstream[len("file/") :]
|
| 61 |
+
if rest.startswith("bot"):
|
| 62 |
+
token_enc = rest[3:].split("/", 1)[0] or None
|
| 63 |
+
after_bot = rest.split("/", 1)[1] if "/" in rest else ""
|
| 64 |
+
fixed = _normalize_bot_api_file_path(after_bot, token_enc)
|
| 65 |
+
if fixed:
|
| 66 |
+
path_for_upstream = f"file/bot{token_enc}/{fixed}"
|
| 67 |
+
|
| 68 |
+
url = f"{UPSTREAM}/{path_for_upstream}"
|
| 69 |
|
| 70 |
params = list(request.query_params.multi_items())
|
| 71 |
|
|
|
|
| 79 |
# 关键修复:
|
| 80 |
# - Bot API 的 /bot... 响应是小 JSON(sendDocument/getFile/getMe),直接“读全量再返回”,避免流式上下文提前关闭导致空响应。
|
| 81 |
# - Bot API 的 /file/... 响应是大文件,才需要流式透传。
|
| 82 |
+
is_file_download = path_for_upstream.startswith("file/")
|
| 83 |
+
is_get_file = "/getFile" in ("/" + path_for_upstream)
|
| 84 |
|
| 85 |
passthrough_allow = {
|
| 86 |
"content-type",
|
|
|
|
| 118 |
if isinstance(payload, dict) and payload.get("ok") is True and isinstance(payload.get("result"), dict):
|
| 119 |
result = payload.get("result") or {}
|
| 120 |
fp = result.get("file_path")
|
| 121 |
+
token_enc = None
|
| 122 |
+
p = path_for_upstream.lstrip("/")
|
| 123 |
+
if p.startswith("bot"):
|
| 124 |
+
token_enc = p[3:].split("/", 1)[0] or None
|
| 125 |
+
|
| 126 |
+
fixed = _normalize_bot_api_file_path(fp if isinstance(fp, str) else None, token_enc)
|
| 127 |
+
if fixed:
|
| 128 |
+
result["file_path"] = fixed
|
| 129 |
+
payload["result"] = result
|
| 130 |
+
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
| 131 |
+
# 确保 JSON 的 content-type 不被冲掉
|
| 132 |
+
resp_headers.setdefault("content-type", "application/json")
|
| 133 |
+
return Response(content=body, status_code=r.status_code, headers=resp_headers)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
except Exception:
|
| 135 |
pass
|
| 136 |
|