LihonT 2 Luna pre-release
Avatar shapes (circle, flower, rounded square, hexagon, heart, triangle): the own shape everywhere and, through the members service, the shapes other members published after proving the account with the code in the bio. Own accent colour and AMOLED, compact bottom bar, Material 3 profile and plus button, save menu (quality, audio only, photo posts without the watermark), fixed comment cards, "Luna (pre-release)" in the mod screen. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,10 @@ 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.
|
||||
|
||||
Avatar outlines are different: one is published for an account only after that
|
||||
account's public bio held its code (the same proof as the niche badge), and the
|
||||
installation that proved it keeps the right to change it.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
@@ -26,6 +30,8 @@ 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")
|
||||
# Avatar outlines a member can publish; the round one is TikTok's own and is never stored.
|
||||
SHAPES = ("flower", "squircle", "hexagon", "heart", "triangle")
|
||||
# The niche-mod badge can be claimed until this moment (Moscow time).
|
||||
CLAIM_DEADLINE = 1794735240 # 2026-11-15 12:34:00 +03:00
|
||||
|
||||
@@ -65,6 +71,8 @@ class Registry:
|
||||
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 shapes (uid TEXT PRIMARY KEY, shape TEXT NOT NULL, updated_at INTEGER NOT NULL)")
|
||||
db.execute("CREATE TABLE IF NOT EXISTS shape_owner (uid TEXT PRIMARY KEY, installation TEXT NOT NULL, unique_id TEXT NOT NULL, verified_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),))
|
||||
|
||||
@@ -133,6 +141,54 @@ class Registry:
|
||||
(uid, unique_id, int(self.clock())))
|
||||
return {"ok": True, "granted": True}
|
||||
|
||||
def shape(self, uid, installation, shape, unique_id=None):
|
||||
"""Publishes (or, for "circle", withdraws) the avatar outline of `uid`.
|
||||
|
||||
The installation that proved the account stays bound to it, so that installation changes the outline
|
||||
again without another proof. Any other installation has to prove the account first: its public bio
|
||||
must hold the account's code, exactly as for the niche badge.
|
||||
"""
|
||||
if not valid_uid(uid) or not isinstance(installation, str) or not INSTALL.fullmatch(installation):
|
||||
raise ValueError("Invalid account ID or installation code")
|
||||
if shape != "circle" and shape not in SHAPES:
|
||||
raise ValueError("Unknown shape")
|
||||
digest = hashlib.sha256(installation.encode("ascii")).hexdigest()
|
||||
with self.connect() as db:
|
||||
if db.execute("SELECT 1 FROM blocked WHERE uid=?", (uid,)).fetchone():
|
||||
raise ValueError("Account excluded")
|
||||
owner = db.execute("SELECT installation FROM shape_owner WHERE uid=?", (uid,)).fetchone()
|
||||
has_shape = db.execute("SELECT 1 FROM shapes WHERE uid=?", (uid,)).fetchone()
|
||||
proven = owner is not None and hmac.compare_digest(owner[0], digest)
|
||||
if not proven and shape == "circle" and not has_shape:
|
||||
return {"ok": True, "published": True, "shape": "circle"} # nothing to withdraw
|
||||
if not proven:
|
||||
if not isinstance(unique_id, str) or not UNIQUE.fullmatch(unique_id):
|
||||
return {"ok": True, "published": False, "reason": "proof_needed", "code": self.code(uid)}
|
||||
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, "published": False, "reason": "code_missing", "code": self.code(uid)}
|
||||
with self.connect() as db:
|
||||
db.execute("INSERT INTO shape_owner(uid,installation,unique_id,verified_at) VALUES(?,?,?,?) "
|
||||
"ON CONFLICT(uid) DO UPDATE SET installation=excluded.installation,"
|
||||
"unique_id=excluded.unique_id,verified_at=excluded.verified_at",
|
||||
(uid, digest, unique_id, int(self.clock())))
|
||||
with self.connect() as db:
|
||||
if shape == "circle":
|
||||
db.execute("DELETE FROM shapes WHERE uid=?", (uid,))
|
||||
else:
|
||||
db.execute("INSERT INTO shapes(uid,shape,updated_at) VALUES(?,?,?) "
|
||||
"ON CONFLICT(uid) DO UPDATE SET shape=excluded.shape,updated_at=excluded.updated_at",
|
||||
(uid, shape, int(self.clock())))
|
||||
return {"ok": True, "published": True, "shape": shape}
|
||||
|
||||
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")
|
||||
@@ -148,12 +204,15 @@ class Registry:
|
||||
supporters = {row[0] for row in db.execute(
|
||||
"SELECT uid FROM supporters WHERE uid IN (" + placeholders + ")", uids
|
||||
).fetchall()}
|
||||
shapes = {row[0]: row[1] for row in db.execute(
|
||||
"SELECT uid,shape FROM shapes WHERE uid IN (" + placeholders + ") AND uid NOT IN (SELECT uid FROM blocked)", uids
|
||||
).fetchall() if row[1] in SHAPES}
|
||||
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}
|
||||
"niche": {uid: uid in niche for uid in uids}, "shapes": shapes, "ttl": 300}
|
||||
|
||||
def set_supporter(self, uid, enabled=True):
|
||||
"""Admin-only when invoked from the server CLI; never exposed over HTTP."""
|
||||
@@ -175,7 +234,8 @@ class Limits:
|
||||
|
||||
def allow(self, ip, operation):
|
||||
key = ip, operation
|
||||
window, count = (60, 120) if operation == "lookup" else (3600, 10) if operation == "claim" else (3600, 40)
|
||||
window, count = ((60, 120) if operation == "lookup" else (3600, 10) if operation == "claim"
|
||||
else (3600, 30) if operation == "shape" else (3600, 40))
|
||||
now = time.monotonic()
|
||||
with self.lock:
|
||||
if len(self.events) > 10000:
|
||||
@@ -215,11 +275,11 @@ class Handler(BaseHTTPRequestHandler):
|
||||
|
||||
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"):
|
||||
if self.path != "/members/v1/" + operation or operation not in ("presence", "lookup", "withdraw", "code", "claim", "shape"):
|
||||
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"):
|
||||
if not self.server.limits.allow(ip, operation if operation in ("lookup", "claim", "shape") else "presence"):
|
||||
return self.send_json(429, {"error": "Try again later"})
|
||||
try:
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
@@ -236,6 +296,9 @@ class Handler(BaseHTTPRequestHandler):
|
||||
"deadline": CLAIM_DEADLINE, "open": time.time() <= CLAIM_DEADLINE}
|
||||
elif operation == "claim":
|
||||
result = self.server.registry.claim(payload.get("uid"), payload.get("unique_id"))
|
||||
elif operation == "shape":
|
||||
result = self.server.registry.shape(payload.get("uid"), payload.get("installation"),
|
||||
payload.get("shape"), 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)
|
||||
|
||||
Reference in New Issue
Block a user