ZaStoGram/Tools/check_zasto_no_ads_contract.py
loop-uh dadaf092de
Some checks failed
Build three ZaStoGram APKs / build (arm64-v8a, ZaStoGram-standalone-arm64-v8a, Arm64, arm64) (push) Has been cancelled
Build three ZaStoGram APKs / build (armeabi-v7a, ZaStoGram-standalone-armeabi-v7a, Armv7, armv7) (push) Has been cancelled
Build three ZaStoGram APKs / build (x86, ZaStoGram-standalone-x86, X86, x86) (push) Has been cancelled
ZaStoGram source guards / guards (push) Has been cancelled
Резать WSS-кадры по границам MTProto-пакетов
Официальный релей разбирает только ПЕРВЫЙ MTProto-пакет каждого
WebSocket-кадра и молча выбрасывает всё, что идёт следом. Проверено
живыми соединениями 9 августа 2026: msgs_ack и req_DH_params, отправленные
двумя кадрами, дают server_DH_params_ok, а те же два пакета, склеенные в
один кадр, не получают ответа вовсе — ни ошибки, ни закрытия.

Именно это ломало создание ключа для датацентра: перед вторым шагом
рукопожатия клиент шлёт подтверждение (Handshake.cpp), байтовый транспорт
склеивал его со следующим пакетом, и рукопожатие висело вечно. Без ключа
все запросы к датацентру оставались в очереди, а картинки оттуда не
грузились при внешне рабочем соединении.

Предыдущий вывод «границы MTProto-пакетов роли не играют» был ошибочным:
он опирался на проверки, где в кадре был ровно один пакет, и потеря
второго была не видна. Теперь размеры пакетов записываются при постановке
в очередь, а кадр выпускается ровно по границе; страж это фиксирует.

Заодно страж прогрева прокси приведён к новой схеме дедупликации логов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 14:22:17 +03:00

141 lines
6.5 KiB
Python

#!/usr/bin/env python3
"""Static guard for ZaStoGram no-ads behavior."""
from pathlib import Path
import re
import sys
ROOT = Path(__file__).resolve().parents[1]
ZASTO_PRIVACY = ROOT / "TMessagesProj/src/main/java/org/telegram/messenger/ZaStoPrivacy.java"
MESSAGES_CONTROLLER = ROOT / "TMessagesProj/src/main/java/org/telegram/messenger/MessagesController.java"
CHAT_ACTIVITY = ROOT / "TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java"
VIDEO_ADS = ROOT / "TMessagesProj/src/main/java/org/telegram/messenger/video/VideoAds.java"
DIALOGS_SEARCH_ADAPTER = ROOT / "TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsSearchAdapter.java"
DIALOGS_ADAPTER = ROOT / "TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsAdapter.java"
def read(path: Path) -> str:
return path.read_text(encoding="utf-8", errors="replace")
def method_body(text: str, signature: str) -> str:
start = text.find(signature)
if start < 0:
return ""
brace = text.find("{", start)
if brace < 0:
return ""
depth = 0
for index in range(brace, len(text)):
char = text[index]
if char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
return text[start:index + 1]
return ""
def main() -> int:
zasto_privacy = read(ZASTO_PRIVACY)
messages_controller = read(MESSAGES_CONTROLLER)
chat_activity = read(CHAT_ACTIVITY)
video_ads = read(VIDEO_ADS)
dialogs_search_adapter = read(DIALOGS_SEARCH_ADAPTER)
dialogs_adapter = read(DIALOGS_ADAPTER)
failures: list[str] = []
def require(condition: bool, message: str) -> None:
if not condition:
failures.append(message)
require(
"public static boolean DISABLE_ADS = true;" in zasto_privacy,
"ZaStoPrivacy must expose the DISABLE_ADS runtime toggle enabled by default",
)
get_sponsored = method_body(messages_controller, "public SponsoredMessagesInfo getSponsoredMessages(long dialogId)")
check_promo = method_body(messages_controller, "private void checkPromoInfoInternal(boolean reset)")
require(
"ZaStoPrivacy.DISABLE_ADS" in check_promo
and "promoDialogId = 0" in check_promo
and check_promo.find("ZaStoPrivacy.DISABLE_ADS") < check_promo.find("new TLRPC.TL_help_getPromoData"),
"MessagesController.checkPromoInfoInternal must return before the promo-data network request",
)
require(
"ZaStoPrivacy.DISABLE_ADS" in get_sponsored
and "sponsoredMessages.remove(dialogId)" in get_sponsored
and get_sponsored.find("ZaStoPrivacy.DISABLE_ADS") < get_sponsored.find("new TLRPC.TL_messages_getSponsoredMessages"),
"MessagesController.getSponsoredMessages must return before the sponsored network request",
)
require(
re.search(r"public\s+boolean\s+isSponsoredDisabled\s*\(\s*\)\s*\{\s*if\s*\(ZaStoPrivacy\.DISABLE_ADS\)\s+return\s+true;", messages_controller) is not None,
"MessagesController.isSponsoredDisabled must report disabled ads while DISABLE_ADS is enabled",
)
require(
re.search(r"public\s+boolean\s+isPromoDialog\s*\([^)]*\)\s*\{\s*if\s*\(ZaStoPrivacy\.DISABLE_ADS\)\s+return\s+false;", messages_controller) is not None,
"MessagesController.isPromoDialog must hide proxy/PSA promo dialogs while DISABLE_ADS is enabled",
)
add_sponsored = method_body(chat_activity, "private void addSponsoredMessages(boolean animated)")
require(
"ZaStoPrivacy.DISABLE_ADS" in add_sponsored
and add_sponsored.find("ZaStoPrivacy.DISABLE_ADS") < add_sponsored.find("getMessagesController().getSponsoredMessages"),
"ChatActivity.addSponsoredMessages must return before adding channel/bot sponsored messages",
)
update_top_panel = method_body(chat_activity, "public void updateTopPanel(boolean animated)")
require(
"ZaStoPrivacy.DISABLE_ADS" in update_top_panel,
"ChatActivity top panel must not show bot sponsored ads while DISABLE_ADS is enabled",
)
require(
"if (org.telegram.messenger.ZaStoPrivacy.DISABLE_ADS) return;" in method_body(chat_activity, "public void logSponsoredClicked")
and "if (org.telegram.messenger.ZaStoPrivacy.DISABLE_ADS) return;" in method_body(chat_activity, "private void markSponsoredAsRead"),
"ChatActivity must not log sponsored impressions/clicks while DISABLE_ADS is enabled",
)
load_video_ads = method_body(video_ads, "private void load()")
require(
"ZaStoPrivacy.DISABLE_ADS" in video_ads
and "loaded = true" in load_video_ads
and load_video_ads.find("ZaStoPrivacy.DISABLE_ADS") < load_video_ads.find("new TLRPC.TL_messages_getSponsoredMessages"),
"VideoAds.load must complete without a sponsored network request while DISABLE_ADS is enabled",
)
require(
"if (ZaStoPrivacy.DISABLE_ADS || ad == null) return;" in method_body(video_ads, "public void logSponsoredShown")
and "if (ZaStoPrivacy.DISABLE_ADS || ad == null) return;" in method_body(video_ads, "public void logSponsoredClicked"),
"VideoAds must not log sponsored impressions/clicks while DISABLE_ADS is enabled",
)
search_method = method_body(dialogs_search_adapter, "public void searchDialogs")
require(
"ZaStoPrivacy.DISABLE_ADS" in dialogs_search_adapter
and "sponsoredPeers.clear()" in search_method
and search_method.find("ZaStoPrivacy.DISABLE_ADS") < search_method.find("new TLRPC.TL_contacts_getSponsoredPeers"),
"DialogsSearchAdapter must avoid sponsored peer requests while DISABLE_ADS is enabled",
)
require(
"if (ZaStoPrivacy.DISABLE_ADS || sponsoredPeer == null) return;" in method_body(dialogs_search_adapter, "public void seenSponsoredPeer")
and "if (ZaStoPrivacy.DISABLE_ADS || sponsoredPeer == null) return;" in method_body(dialogs_search_adapter, "public void clickedSponsoredPeer"),
"DialogsSearchAdapter must not log sponsored peer impressions/clicks while DISABLE_ADS is enabled",
)
require(
"ZaStoPrivacy.DISABLE_ADS" in method_body(dialogs_adapter, "private ArrayList<TLRPC.Dialog> filterLegacyProxySponsorDialogs"),
"DialogsAdapter must remove legacy Telegram proxy sponsor dialogs while DISABLE_ADS is enabled",
)
if failures:
print("ZaSto no-ads contract failed:", file=sys.stderr)
for failure in failures:
print(f" - {failure}", file=sys.stderr)
return 1
print("ZaSto no-ads contract passed.")
return 0
if __name__ == "__main__":
raise SystemExit(main())