sync: update from private repo (2336949a)
CI / build-and-test (push) Canceled after 0s

This commit is contained in:
oss-sync
2026-09-17 11:29:31 +00:00
parent 28f74c4e37
commit 57702b4f34
2139 changed files with 321270 additions and 17494 deletions
+115 -4
View File
@@ -402,6 +402,108 @@ def _silence_logs() -> None:
pass
# ── txid: indices スクリプトの深掘り探索 ──────────────────────────────
#
# 2026-07 に X が未ログインページを x-web の SSR 版へ寄せた。ページ直下の <script> は
# entry チャンク 1 本だけになり、署名用 indices ファイル (sign.o-*.js) はその entry が
# 動的 import する assets/*.js のさらに中から参照される形に変わった。
# twscrape 0.19.1 の _find_indices_url は 1 段目しか走査しないので到達できず、
# "Couldn't get XClientTxId indices script" で txid 生成が落ちる。txid が無いリクエストは
# X 側が黙って 0 件を返すため、症状は「エラー無しで常に空」になる (上流 issue #320)。
#
# 上流 PR #321 は「cookie 付きで取れば indices を含むバンドルが来る」という案だが、
# 実測ではこちらの認証セッションには旧 responsive-web バンドルが配信され、そこに
# indices は含まれていなかった。よって cookie 経路には頼らず、未ログインのまま
# もう 1 ホップ潜る。上流ロジックは残し、この関数だけ差し替える。
CHUNK_SCAN_CAP = 200 # 1 段目 1 チャンクあたりに追跡するサブチャンク数の上限
CHUNK_FETCH_CONCURRENCY = 16
# バンドル内の相対 import (`"./assets/x-Hash.js"` / `'../messages/ja.js'`) を拾う。
# 絶対 URL は別オリジンを踏みに行かないよう意図的に対象外。
_CHUNK_REF_RE = re.compile(r"""["'(](\.{1,2}/[\w./-]*?\.js)["')]""")
# indices ファイル名。上流 twscrape の INDICES_FILE_RE と同義だが、テストを
# twscrape 非依存で回すため自前に持つ (上流のリネームからも切り離される)。
# \b は design.o-*.js のような部分一致除け。
INDICES_FILE_RE = re.compile(r"(?:\.{0,2}/)?[\w./-]*?\b(?:ondemand\.s|sign\.o)[\w.-]*\.js")
TXID_STATE = {"failed": False}
def collect_chunk_refs(body: str, base_url: str) -> list[str]:
"""チャンク本文から、次に辿るべきサブチャンクの絶対 URL 一覧を作る。
indices は assets/ 配下にあるので assets/ を含む参照を先に試す (i18n の
../messages/*.js が数百件あり、後回しにしないと無駄打ちが増える)。
"""
from urllib.parse import urljoin
seen, assets, others = set(), [], []
for m in _CHUNK_REF_RE.finditer(body):
url = urljoin(base_url, m.group(1))
if url in seen:
continue
seen.add(url)
(assets if "/assets/" in url else others).append(url)
return (assets + others)[:CHUNK_SCAN_CAP]
async def find_indices_url_deep(scripts: list, clt) -> str:
"""indices ファイル (sign.o-*.js / ondemand.s*.js) の URL を解決する。
1 段目 = 上流と同じくページ直下のスクリプト本文を走査。見つからなければ
2 段目 = 各スクリプトが import している相対チャンクを走査する。
どこにも無ければ TXID_STATE に記録した上で例外を上げる (握り潰さない)。
"""
import asyncio
from urllib.parse import urljoin
sem = asyncio.Semaphore(CHUNK_FETCH_CONCURRENCY)
async def fetch(url: str) -> tuple[str, str]:
async with sem:
try:
return url, (await clt.get(url)).text
except Exception:
return url, ""
async def scan(urls: list) -> tuple[str, list]:
"""走査してヒット URL を返す。未ヒットなら次段の候補も併せて返す。"""
nxt = []
tasks = [asyncio.ensure_future(fetch(u)) for u in urls]
try:
for fut in asyncio.as_completed(tasks):
url, body = await fut
m = INDICES_FILE_RE.search(body)
if m:
return urljoin(url, m.group(0)), []
nxt.extend(collect_chunk_refs(body, url))
finally:
for t in tasks:
t.cancel()
return "", nxt
hit, level2 = await scan(list(scripts))
if not hit and level2:
hit, _ = await scan(list(dict.fromkeys(level2)))
if not hit:
TXID_STATE["failed"] = True
raise Exception("Couldn't get XClientTxId indices script")
TXID_STATE["failed"] = False
return hit
def _install_xclid_deep_scan() -> None:
"""twscrape の indices 探索を深掘り版へ差し替える (import 済みが前提)。"""
try:
import twscrape.xclid as _xclid
_xclid._find_indices_url = find_indices_url_deep
except Exception:
pass
# ── backend (twscrape / twikit、遅延 import) ──────────────────────────
def _creds() -> tuple[str, str]:
@@ -441,12 +543,19 @@ def _cleanup_stale_tmp() -> None:
async def _ensure_alive(api) -> None:
"""直近のリクエストでアカウントが 401/banned 扱いになっていないか確認。
"""空結果が『本当に0件』か『取得できていない』かを切り分け、後者は nonzero で落とす。
twscrape は 401/429 を例外にせずログして停止し、空イテレーションを返す。空結果が
『本当に0件』か『認証切れ/レート制限』かを区別するため、停止時に無効化される
account.active を見て、無効なら nonzero で fail する (x.ts がエラー表示)。
twscrape は 401/429 も txid 生成失敗も例外にせずログして停止し、空イテレーションを
返す。黙って空を返すと利用者からは「エラーも出ないのに何も取れない」に見えるので、
原因が分かる形で fail させる (x.ts がそのまま表示する)。
- txid 生成失敗 … X のバンドル構成変更。TXID_STATE で検知
- 認証切れ/BAN/レート制限 … 停止時に account.active が落ちる
"""
if TXID_STATE.get("failed"):
fail("x-adapter: could not build the X request signature (x-client-transaction-id); "
"X changed its web bundle layout again. Every request returns empty until the "
"indices lookup is updated. See scripts/x-adapter/x_adapter.py "
"(find_indices_url_deep) and twscrape upstream issue #320.")
try:
accs = await api.pool.get_all()
except Exception:
@@ -463,6 +572,7 @@ async def _make_api():
import tempfile
from twscrape import API
_silence_logs() # twscrape import 後に呼ぶ (import 時に loguru sink が張られるため)
_install_xclid_deep_scan()
auth, ct0 = _creds()
tmpdir = tempfile.mkdtemp(prefix=_TMP_PREFIX)
db = os.path.join(tmpdir, "accounts.db")
@@ -601,6 +711,7 @@ async def cmd_feed(args) -> None:
from twikit.client.gql import Endpoint
from twikit.constants import FEATURES
_silence_logs() # twscrape import 後に呼ぶ
_install_xclid_deep_scan() # feed は _make_api を通らないのでここでも要適用
auth, ct0 = _creds()
following = (args.tab or "").strip().lower() == "following"