LihonT 2 Luna beta: source release
Mod for TikTok 47.1.3 (arm64) in Material 3 style: bottom bar, profile menu, TikTok settings and comments; downloads without the watermark; switches for ads, live streams and the Shop tab; badges of other mods next to names and the niche mod badge (members service); app icons; mod settings screen. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
268
services/members/server.py
Normal file
268
services/members/server.py
Normal file
@@ -0,0 +1,268 @@
|
||||
"""Self-reported LihonT presence registry. Standard library only; bind to localhost.
|
||||
|
||||
This is a community marker, not proof of TikTok account ownership or official
|
||||
verification. A modified client can claim another UID. No TikTok credentials
|
||||
are accepted. Installation secrets permit withdrawing only that installation's
|
||||
contribution. Public lookup responses expose only requested membership bits.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import base64
|
||||
import hmac
|
||||
import secrets
|
||||
import urllib.request
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import sqlite3
|
||||
import threading
|
||||
import time
|
||||
from collections import defaultdict, deque
|
||||
from contextlib import contextmanager
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
UID = re.compile(r"[1-9][0-9]{4,24}\Z")
|
||||
INSTALL = re.compile(r"[a-f0-9]{64}\Z")
|
||||
LIFETIME = 30 * 86400
|
||||
UNIQUE = re.compile(r"[A-Za-z0-9_.]{2,24}\Z")
|
||||
# The niche-mod badge can be claimed until this moment (Moscow time).
|
||||
CLAIM_DEADLINE = 1794735240 # 2026-11-15 12:34:00 +03:00
|
||||
|
||||
|
||||
def fetch_profile(unique_id):
|
||||
"""Public TikTok profile page of @unique_id as (account id, bio), or None. Anonymous GET only."""
|
||||
request = urllib.request.Request(
|
||||
"https://www.tiktok.com/@" + unique_id,
|
||||
headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Chrome/124.0 Safari/537.36"})
|
||||
with urllib.request.urlopen(request, timeout=20) as response:
|
||||
page = response.read(3_000_000).decode("utf-8", "replace")
|
||||
match = re.search(r'<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__"[^>]*>(.*?)</script>', page, re.S)
|
||||
if not match:
|
||||
return None
|
||||
user = (json.loads(match.group(1)).get("__DEFAULT_SCOPE__", {})
|
||||
.get("webapp.user-detail", {}).get("userInfo", {}).get("user", {}))
|
||||
if not user.get("id"):
|
||||
return None
|
||||
return str(user["id"]), str(user.get("signature") or "")
|
||||
|
||||
|
||||
def valid_uid(value):
|
||||
return isinstance(value, str) and UID.fullmatch(value) is not None
|
||||
|
||||
|
||||
class Registry:
|
||||
def __init__(self, path, clock=time.time, fetcher=fetch_profile):
|
||||
self.path, self.clock, self.fetcher = str(path), clock, fetcher
|
||||
with self.connect() as db:
|
||||
db.execute("PRAGMA journal_mode=WAL")
|
||||
db.execute("CREATE TABLE IF NOT EXISTS presence (installation TEXT NOT NULL, uid TEXT NOT NULL, last_seen INTEGER NOT NULL, frame_color TEXT NOT NULL DEFAULT 'green', PRIMARY KEY(installation, uid))")
|
||||
columns = {row[1] for row in db.execute("PRAGMA table_info(presence)")}
|
||||
if "frame_color" not in columns:
|
||||
db.execute("ALTER TABLE presence ADD COLUMN frame_color TEXT NOT NULL DEFAULT 'green'")
|
||||
db.execute("CREATE INDEX IF NOT EXISTS presence_uid ON presence(uid, last_seen)")
|
||||
db.execute("CREATE TABLE IF NOT EXISTS blocked (uid TEXT PRIMARY KEY)")
|
||||
db.execute("CREATE TABLE IF NOT EXISTS supporters (uid TEXT PRIMARY KEY, granted_at INTEGER NOT NULL)")
|
||||
db.execute("CREATE TABLE IF NOT EXISTS niche (uid TEXT PRIMARY KEY, unique_id TEXT NOT NULL, granted_at INTEGER NOT NULL)")
|
||||
db.execute("CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)")
|
||||
db.execute("INSERT OR IGNORE INTO meta(key,value) VALUES('code_secret',?)", (secrets.token_hex(32),))
|
||||
|
||||
@contextmanager
|
||||
def connect(self):
|
||||
db = sqlite3.connect(self.path, timeout=10)
|
||||
try:
|
||||
with db:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def presence(self, uid, installation, withdraw=False, frame_color="green"):
|
||||
if not valid_uid(uid) or not isinstance(installation, str) or not INSTALL.fullmatch(installation):
|
||||
raise ValueError("Invalid account ID or installation code")
|
||||
digest = hashlib.sha256(installation.encode("ascii")).hexdigest()
|
||||
if frame_color not in ("none", "green", "red", "yellow"):
|
||||
frame_color = "green"
|
||||
now = int(self.clock())
|
||||
with self.connect() as db:
|
||||
db.execute("DELETE FROM presence WHERE last_seen < ?", (now - LIFETIME,))
|
||||
if withdraw:
|
||||
db.execute("DELETE FROM presence WHERE installation=? AND uid=?", (digest, uid))
|
||||
else:
|
||||
if db.execute("SELECT 1 FROM blocked WHERE uid=?", (uid,)).fetchone():
|
||||
raise ValueError("Account excluded from the registry")
|
||||
existing = db.execute("SELECT 1 FROM presence WHERE installation=? AND uid=?", (digest, uid)).fetchone()
|
||||
if not existing and db.execute("SELECT COUNT(*) FROM presence WHERE installation=?", (digest,)).fetchone()[0] >= 20:
|
||||
raise ValueError("Too many accounts for one installation")
|
||||
db.execute("INSERT INTO presence(installation,uid,last_seen,frame_color) VALUES(?,?,?,?) ON CONFLICT(installation,uid) DO UPDATE SET last_seen=excluded.last_seen,frame_color=excluded.frame_color", (digest, uid, now, frame_color))
|
||||
return {"ok": True}
|
||||
|
||||
def code(self, uid):
|
||||
"""The proof string for an account: it only ever goes into that account's own bio."""
|
||||
if not valid_uid(uid):
|
||||
raise ValueError("Invalid account ID")
|
||||
with self.connect() as db:
|
||||
secret = db.execute("SELECT value FROM meta WHERE key='code_secret'").fetchone()[0]
|
||||
digest = hmac.new(bytes.fromhex(secret), uid.encode("ascii"), "sha256").digest()
|
||||
return "LT-" + base64.b32encode(digest)[:8].decode("ascii")
|
||||
|
||||
def claim(self, uid, unique_id):
|
||||
"""Gives the niche-mod badge to `uid` when its public bio holds the account's code."""
|
||||
if not valid_uid(uid) or not isinstance(unique_id, str) or not UNIQUE.fullmatch(unique_id):
|
||||
raise ValueError("Invalid account")
|
||||
with self.connect() as db:
|
||||
if db.execute("SELECT 1 FROM niche WHERE uid=?", (uid,)).fetchone():
|
||||
return {"ok": True, "granted": True}
|
||||
if db.execute("SELECT 1 FROM blocked WHERE uid=?", (uid,)).fetchone():
|
||||
raise ValueError("Account excluded")
|
||||
if self.clock() > CLAIM_DEADLINE:
|
||||
raise ValueError("The badge can no longer be claimed")
|
||||
try:
|
||||
profile = self.fetcher(unique_id)
|
||||
except Exception:
|
||||
raise ValueError("Could not read the TikTok profile, try again later")
|
||||
if profile is None:
|
||||
raise ValueError("Profile not found")
|
||||
found_uid, bio = profile
|
||||
if found_uid != uid:
|
||||
raise ValueError("That profile belongs to another account")
|
||||
if self.code(uid) not in bio:
|
||||
return {"ok": True, "granted": False, "reason": "code_missing"}
|
||||
with self.connect() as db:
|
||||
db.execute("INSERT OR IGNORE INTO niche(uid,unique_id,granted_at) VALUES(?,?,?)",
|
||||
(uid, unique_id, int(self.clock())))
|
||||
return {"ok": True, "granted": True}
|
||||
|
||||
def lookup(self, uids):
|
||||
if not isinstance(uids, list) or len(uids) > 100 or any(not valid_uid(uid) for uid in uids):
|
||||
raise ValueError("Request at most 100 account IDs")
|
||||
uids = list(dict.fromkeys(uids))
|
||||
if not uids:
|
||||
return {"members": {}}
|
||||
placeholders = ",".join("?" for _ in uids)
|
||||
with self.connect() as db:
|
||||
rows = db.execute("SELECT uid,frame_color FROM presence WHERE uid IN (" + placeholders + ") AND last_seen >= ? AND uid NOT IN (SELECT uid FROM blocked)", uids + [int(self.clock()) - LIFETIME]).fetchall()
|
||||
niche = {row[0] for row in db.execute(
|
||||
"SELECT uid FROM niche WHERE uid IN (" + placeholders + ") AND uid NOT IN (SELECT uid FROM blocked)", uids
|
||||
).fetchall()}
|
||||
supporters = {row[0] for row in db.execute(
|
||||
"SELECT uid FROM supporters WHERE uid IN (" + placeholders + ")", uids
|
||||
).fetchall()}
|
||||
colors = {}
|
||||
for uid, color in rows:
|
||||
colors.setdefault(uid, color if color in ("none", "green", "red", "yellow") else "green")
|
||||
return {"members": {uid: uid in colors for uid in uids}, "frames": colors,
|
||||
"supporters": {uid: uid in supporters for uid in uids},
|
||||
"niche": {uid: uid in niche for uid in uids}, "ttl": 300}
|
||||
|
||||
def set_supporter(self, uid, enabled=True):
|
||||
"""Admin-only when invoked from the server CLI; never exposed over HTTP."""
|
||||
if not valid_uid(uid):
|
||||
raise ValueError("Invalid account ID")
|
||||
with self.connect() as db:
|
||||
if enabled:
|
||||
db.execute("INSERT OR IGNORE INTO supporters(uid,granted_at) VALUES(?,?)",
|
||||
(uid, int(self.clock())))
|
||||
else:
|
||||
db.execute("DELETE FROM supporters WHERE uid=?", (uid,))
|
||||
return {"uid": uid, "supporter": bool(enabled)}
|
||||
|
||||
|
||||
class Limits:
|
||||
def __init__(self):
|
||||
self.events = defaultdict(deque)
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def allow(self, ip, operation):
|
||||
key = ip, operation
|
||||
window, count = (60, 120) if operation == "lookup" else (3600, 10) if operation == "claim" else (3600, 40)
|
||||
now = time.monotonic()
|
||||
with self.lock:
|
||||
if len(self.events) > 10000:
|
||||
for old in list(self.events):
|
||||
if not self.events[old] or self.events[old][-1] < now - 3600:
|
||||
del self.events[old]
|
||||
if len(self.events) > 10000 and key not in self.events:
|
||||
return False
|
||||
queue = self.events[key]
|
||||
while queue and queue[0] < now - window:
|
||||
queue.popleft()
|
||||
if len(queue) >= count:
|
||||
return False
|
||||
queue.append(now)
|
||||
return True
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
server_version = "LihonT-Members/1"
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
# Do not log payloads, installation codes or account IDs.
|
||||
pass
|
||||
|
||||
def send_json(self, status, payload):
|
||||
raw = json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.send_header("Content-Length", str(len(raw)))
|
||||
self.end_headers()
|
||||
self.wfile.write(raw)
|
||||
|
||||
def do_GET(self):
|
||||
self.send_json(200 if self.path == "/members/v1/health" else 404,
|
||||
{"ok": True} if self.path == "/members/v1/health" else {"error": "Not found"})
|
||||
|
||||
def do_POST(self):
|
||||
operation = self.path.removeprefix("/members/v1/")
|
||||
if self.path != "/members/v1/" + operation or operation not in ("presence", "lookup", "withdraw", "code", "claim"):
|
||||
return self.send_json(404, {"error": "Not found"})
|
||||
# This server binds to localhost; nginx overwrites this header.
|
||||
ip = self.headers.get("X-Real-IP", self.client_address[0])
|
||||
if not self.server.limits.allow(ip, operation if operation in ("lookup", "claim") else "presence"):
|
||||
return self.send_json(429, {"error": "Try again later"})
|
||||
try:
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
if not 0 < length <= 8192:
|
||||
return self.send_json(413, {"error": "Invalid payload size"})
|
||||
self.connection.settimeout(10)
|
||||
payload = json.loads(self.rfile.read(length))
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("Expected an object")
|
||||
if operation == "lookup":
|
||||
result = self.server.registry.lookup(payload.get("uids"))
|
||||
elif operation == "code":
|
||||
result = {"code": self.server.registry.code(payload.get("uid")),
|
||||
"deadline": CLAIM_DEADLINE, "open": time.time() <= CLAIM_DEADLINE}
|
||||
elif operation == "claim":
|
||||
result = self.server.registry.claim(payload.get("uid"), payload.get("unique_id"))
|
||||
else:
|
||||
result = self.server.registry.presence(payload.get("uid"), payload.get("installation"), operation == "withdraw", payload.get("frame_color", "green"))
|
||||
self.send_json(200, result)
|
||||
except (ValueError, TypeError, UnicodeError) as exc:
|
||||
self.send_json(400, {"error": str(exc)})
|
||||
except Exception:
|
||||
self.send_json(503, {"error": "Service temporarily unavailable"})
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--database", required=True)
|
||||
parser.add_argument("--port", type=int, default=18746)
|
||||
parser.add_argument("--grant-supporter", metavar="UID", help="Grant the LihonT supporter badge locally")
|
||||
parser.add_argument("--revoke-supporter", metavar="UID", help="Revoke the LihonT supporter badge locally")
|
||||
args = parser.parse_args()
|
||||
Path(args.database).resolve().parent.mkdir(parents=True, exist_ok=True)
|
||||
if args.grant_supporter or args.revoke_supporter:
|
||||
registry = Registry(args.database)
|
||||
print(json.dumps(registry.set_supporter(args.grant_supporter or args.revoke_supporter,
|
||||
enabled=bool(args.grant_supporter))))
|
||||
return
|
||||
server = ThreadingHTTPServer(("127.0.0.1", args.port), Handler)
|
||||
server.registry = Registry(args.database)
|
||||
server.limits = Limits()
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user