Sign in Sign up
Branches
master
68 lines (55 loc) · 3.2 KB Raw
"""Build a WhatsApp user-created sticker pack of Fuse mixes.

usage: build_pack.py OUT_DIR NAME A.was:B.was [A.was:B.was ...]
Each A:B pair becomes one sticker (A's sticker layer + B's effect, every file copied
byte for byte). Writes OUT_DIR/pack.zip ("<waName>.was" entries + "<packid>.png" tray
icon, the layout of a received STK_PK), thumb.png (252x252) and manifest.tsv (a header line
id/name/publisher/tray, then fileName/emojis/alt text per sticker): the input of
`sendsticker <session> --pack OUT_DIR`.
"""
import base64, hashlib, io, json, os, subprocess, sys, uuid, zipfile

M, E, T, META = "animation/animation.json", "animation/animation_secondary.json", ".trust_token", "animation/animation.json.overridden_metadata"
LOTTIE = "/root/wa-stickers/tools/lottie.min.js"

out, name, pairs = sys.argv[1], sys.argv[2], [p.split(":") for p in sys.argv[3:]]
os.makedirs(out, exist_ok=True)
pack_id = str(uuid.uuid4())


def wa_name(b):
    return base64.b64encode(hashlib.sha256(b).digest()).decode().replace("/", "-") + ".was"


def mix(a_path, b_path):
    a, b = zipfile.ZipFile(a_path), zipfile.ZipFile(b_path)
    buf = io.BytesIO()
    with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z:
        z.writestr("animation/", b"")
        for n, src in [(M, a), (E, b), (M + T, a), (E + T, b), (META, a)]:
            z.writestr(n, src.read(n))
    meta = json.loads(a.read(META))
    return buf.getvalue(), meta, a.read(M).decode()


def render(lottie_json, size, path):
    html = os.path.join(out, "render.html")
    open(html, "w").write(
        f'<!doctype html><html><body style="margin:0;background:transparent">'
        f'<div id="c" style="width:{size}px;height:{size}px"></div><script>{open(LOTTIE).read()}</script>'
        f'<script>const a=lottie.loadAnimation({{container:document.getElementById("c"),renderer:"svg",loop:false,autoplay:false,animationData:{lottie_json}}});'
        f'a.goToAndStop(Math.floor(a.totalFrames*0.45),true);</script></body></html>')
    subprocess.run(["timeout", "60", "chromium", "--headless=new", "--no-sandbox", "--disable-gpu", "--hide-scrollbars",
                    f"--window-size={size},{size}", "--default-background-color=00000000", "--virtual-time-budget=3000",
                    f"--screenshot={path}", f"file://{html}"], check=True, capture_output=True)
    os.remove(html)


rows, files, first_main = [], [], None
for a_path, b_path in pairs:
    data, meta, main_json = mix(a_path, b_path)
    fn = wa_name(data)
    files.append((fn, data))
    rows.append((fn, " ".join(meta.get("emojis", [])), meta.get("accessibility-text", "")))
    first_main = first_main or main_json

tray = f"{pack_id}.png"
render(first_main, 96, os.path.join(out, tray))
render(first_main, 252, os.path.join(out, "thumb.png"))

with zipfile.ZipFile(os.path.join(out, "pack.zip"), "w", zipfile.ZIP_DEFLATED) as z:
    for fn, data in files:
        z.writestr(fn, data)
    z.write(os.path.join(out, tray), tray)
with open(os.path.join(out, "manifest.tsv"), "w") as f:
    f.write(f"{pack_id}\t{name}\tFuse\t{tray}\n")
    for r in rows:
        f.write("\t".join(x.replace("\t", " ").replace("\n", " ") for x in r) + "\n")
print(pack_id, len(files), "stickers,", os.path.getsize(os.path.join(out, "pack.zip")), "bytes")