539 lines
24 KiB
Python
539 lines
24 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Publish all 20 app stacks and all app definitions with Gitea mirror URLs.
|
|
|
|
Uses n_signer via Qubes qrexec (nostr_index=0) to sign events,
|
|
then publishes to all configured relays.
|
|
|
|
Usage:
|
|
python3 app-stacks/publish_all.py
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import websockets
|
|
|
|
QREXEC_TARGET = "nostr_signer"
|
|
QREXEC_SERVICE = "qubes.NsignerRpc"
|
|
RELAYS = [
|
|
"wss://relay.zapstore.dev",
|
|
"wss://laantungir.net/relay",
|
|
"wss://nos.lol",
|
|
"wss://relay.primal.net",
|
|
]
|
|
|
|
# Zapstore community pubkey — required in the `h` tag for stacks to appear
|
|
# in the Zapstore Android app. See app-stacks/README.md for details.
|
|
KZAPSTORE_COMMUNITY_PUBKEY = "acfeaea6e51420e8068fac446ca9d17d7a9ef6a5d20d93894e50fee3d4902a84"
|
|
|
|
# Load real app developer pubkeys (kind 32267 events) so stack `a` tags
|
|
# reference events that actually exist on the relay.
|
|
# Generated by fetching kind 32267 events from wss://relay.zapstore.dev.
|
|
_PUBKEYS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "app_pubkeys.json")
|
|
try:
|
|
with open(_PUBKEYS_PATH) as f:
|
|
APP_PUBKEYS = json.load(f)
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
APP_PUBKEYS = {}
|
|
|
|
# ── All 20 stacks with their apps ──────────────────────────────────
|
|
|
|
STACKS = [
|
|
{
|
|
"d": "privacy-approved-bitcoin-lightning-wallets",
|
|
"name": "Privacy Approved — Bitcoin & Lightning Wallets",
|
|
"description": "Self-custodial Bitcoin and Lightning wallet apps that passed privacy review. No tracking SDKs, minimal permissions, open source.",
|
|
"apps": [
|
|
("app.zeusln.zeus", "ZEUS"),
|
|
("io.nunchuk.android", "Nunchuk"),
|
|
("com.cakewallet.cake_wallet", "Cake Wallet"),
|
|
("com.getalby.mobile", "Alby Go"),
|
|
("com.blitzwallet", "Blitz Wallet"),
|
|
("io.bluewallet.bluewallet", "BlueWallet"),
|
|
("org.electrum.electrum", "Electrum"),
|
|
("com.oubli.wallet", "Oubli"),
|
|
("com.kilombino.mercasats", "MercaSats"),
|
|
("com.magius.lightningreaction", "Lightning Reaction"),
|
|
],
|
|
},
|
|
{
|
|
"d": "privacy-approved-nostr-clients",
|
|
"name": "Privacy Approved — Nostr Clients",
|
|
"description": "Nostr-native social and communication clients that passed privacy review. No tracking SDKs, minimal permissions, open source.",
|
|
"apps": [
|
|
("com.vitorpamplona.amethyst", "Amethyst"),
|
|
("com.greenart7c3.nostrsigner", "Amber"),
|
|
("com.wisp.app", "Wisp"),
|
|
("com.darkwisp.app", "Dark Wisp"),
|
|
("org.parres.whitenoise", "White Noise"),
|
|
("com.nospeak.app", "Nospeak"),
|
|
("org.nostr.nostrord", "Nostrord"),
|
|
("com.shosho.app", "Shosho"),
|
|
("com.traveltelly.app", "TravelTelly"),
|
|
("app.nostrmail.client", "Nmail"),
|
|
("co.openvine.app", "Divine"),
|
|
("com.yakihonne.yakihonne", "YakiHonne"),
|
|
("com.nostrnests.app", "Nests"),
|
|
("app.nostria.twa", "Nostria"),
|
|
("dev.zapstore.alpha", "Zapstore Alpha"),
|
|
("com.pearcal", "PearCal"),
|
|
("com.pearcircle", "PearCircle"),
|
|
("pub.ditto.app", "Ditto"),
|
|
("social.flotilla", "Flotilla"),
|
|
],
|
|
},
|
|
{
|
|
"d": "privacy-approved-secure-messaging",
|
|
"name": "Privacy Approved — Secure Messaging",
|
|
"description": "End-to-end encrypted messaging and email apps that passed privacy review. No tracking SDKs, minimal permissions, open source.",
|
|
"apps": [
|
|
("chat.simplex.app", "SimpleX"),
|
|
("eu.siacs.conversations", "Conversations"),
|
|
("chat.delta", "Delta Chat"),
|
|
("org.atalk.android", "aTalk"),
|
|
("im.vector.app", "Element"),
|
|
("io.element.android.x", "Element X"),
|
|
("net.thunderbird.android", "Thunderbird"),
|
|
("eu.faircode.email", "FairEmail"),
|
|
("de.tutao.tutanota", "Tuta"),
|
|
("com.quietmobile", "Quiet"),
|
|
("spam.blocker", "SpamBlocker"),
|
|
("com.afkanerd.deku", "Deku SMS"),
|
|
],
|
|
},
|
|
{
|
|
"d": "privacy-approved-vpn-privacy-tools",
|
|
"name": "Privacy Approved — VPN & Privacy Tools",
|
|
"description": "VPN clients, firewalls, and network privacy tools that passed privacy review. No tracking SDKs, minimal permissions, open source.",
|
|
"apps": [
|
|
("org.torproject.android", "Orbot"),
|
|
("net.mullvad.mullvadvpn", "Mullvad VPN"),
|
|
("com.tailscale.ipn", "Tailscale"),
|
|
("org.amnezia.vpn", "AmneziaVPN"),
|
|
("ch.protonvpn.android", "Proton VPN"),
|
|
("com.celzero.bravedns", "Rethink"),
|
|
("com.emanuelef.remote_capture", "PCAPdroid"),
|
|
("com.zaneschepke.wireguardautotunnel", "WG Tunnel"),
|
|
("io.github.romanvht.byedpi", "ByeDPI"),
|
|
("xyz.malkki.neostumbler", "NeoStumbler"),
|
|
("eu.darken.sdmse", "SD Maid"),
|
|
("pan.alexander.tordnscrypt.stable", "InviZible Pro"),
|
|
("com.flashsphere.privatednsqs", "Private DNS Quick Setting"),
|
|
],
|
|
},
|
|
{
|
|
"d": "privacy-approved-password-managers-auth",
|
|
"name": "Privacy Approved — Password Managers & Auth",
|
|
"description": "Password managers, 2FA authenticators, and identity tools that passed privacy review. No tracking SDKs, minimal permissions, open source.",
|
|
"apps": [
|
|
("com.x8bit.bitwarden", "Bitwarden"),
|
|
("com.kunzisoft.keepass.libre", "KeePassDX"),
|
|
("com.beemdevelopment.aegis", "Aegis"),
|
|
("pl.lebihan.authnkey", "Authnkey"),
|
|
("io.ente.auth.independent", "Ente Auth"),
|
|
("io.privkey.keep", "Keep"),
|
|
("net.aliasvault.app", "AliasVault"),
|
|
("com.jksalcedo.passvault", "PassVault"),
|
|
("com.jksalcedo.librefind", "LibreFind"),
|
|
],
|
|
},
|
|
{
|
|
"d": "privacy-approved-maps-navigation",
|
|
"name": "Privacy Approved — Maps & Navigation",
|
|
"description": "Offline maps, navigation, and location tools that passed privacy review. No tracking SDKs, minimal permissions, open source.",
|
|
"apps": [
|
|
("app.organicmaps.web", "Organic Maps"),
|
|
("net.osmand.plus", "OsmAnd~"),
|
|
("de.westnordost.streetcomplete", "StreetComplete"),
|
|
("com.comaps.app", "CoMaps"),
|
|
("org.owntracks.android", "OwnTracks"),
|
|
("net.osmtracker", "OSMTracker"),
|
|
],
|
|
},
|
|
{
|
|
"d": "privacy-approved-media-players-streaming",
|
|
"name": "Privacy Approved — Media Players & Streaming",
|
|
"description": "Video and music players, streaming clients that passed privacy review. No tracking SDKs, minimal permissions, open source.",
|
|
"apps": [
|
|
("org.videolan.vlc", "VLC"),
|
|
("org.schabi.newpipe", "NewPipe"),
|
|
("infinityloop1309.newpipeenhanced", "PipePipe"),
|
|
("org.oxycblt.auxio", "Auxio"),
|
|
("com.gokadzev.musify", "Musify"),
|
|
("com.metrolist.music", "Metrolist"),
|
|
("io.freetubeapp.freetube", "FreeTube"),
|
|
("app.zaptrax", "Zaptrax"),
|
|
("app.zappix", "Zappix"),
|
|
("app.marlboroadvance.mpvex", "mpvEx"),
|
|
("com.github.k1rakishou.chan", "KurobaEx"),
|
|
],
|
|
},
|
|
{
|
|
"d": "privacy-approved-productivity-notes",
|
|
"name": "Privacy Approved — Productivity & Notes",
|
|
"description": "Note-taking, task management, and productivity tools that passed privacy review. No tracking SDKs, minimal permissions, open source.",
|
|
"apps": [
|
|
("com.flux", "Flux"),
|
|
("com.streetwriters.notesnook", "Notesnook"),
|
|
("io.github.quillpad", "Quillpad"),
|
|
("com.adilhanney.saber", "Saber"),
|
|
("at.bitfire.davdroid", "DAVx⁵"),
|
|
("com.superproductivity.superproductivity", "Super Productivity"),
|
|
("eu.fliegendurst.triliumdroid", "Trilium Notes"),
|
|
("ch.martinstoeckli.silentnotes", "SilentNotes"),
|
|
("com.dtonon.manent", "Manent"),
|
|
("jp.godzhigella.meiso", "Meiso"),
|
|
("com.nahnah.florid", "Florid"),
|
|
("com.plektos.app", "Plektos"),
|
|
("io.nurunuru.app", "Urn"),
|
|
("com.shub39.grit", "Grit"),
|
|
("com.electricdreams.numo", "Numo"),
|
|
("me.timeto.app", "timeto.me"),
|
|
("com.ferrarid.converterpro", "Converter NOW"),
|
|
],
|
|
},
|
|
{
|
|
"d": "privacy-approved-file-management-cloud-sync",
|
|
"name": "Privacy Approved — File Management & Cloud Sync",
|
|
"description": "Cloud storage, file sync, and file management tools that passed privacy review. No tracking SDKs, minimal permissions, open source.",
|
|
"apps": [
|
|
("com.nextcloud.client", "Nextcloud"),
|
|
("com.github.catfriend1.syncthingfork", "Syncthing-Fork"),
|
|
("com.seafile.seadroid2", "Seafile"),
|
|
("com.example.file_transfer", "File transfer"),
|
|
("org.primftpd", "primitive ftpd"),
|
|
("de.astubenbord.paperless_mobile", "Paperless Mobile"),
|
|
("com.viscouspot.gitsync", "GitSync"),
|
|
("eu.heili.wormhole", "wormhole"),
|
|
("lu.knaff.alain.saf_sftp", "SFTP Documents Provider"),
|
|
("com.akylas.documentscanner", "OSS Document Scanner"),
|
|
],
|
|
},
|
|
{
|
|
"d": "privacy-approved-browsers",
|
|
"name": "Privacy Approved — Browsers",
|
|
"description": "Privacy-focused web browsers that passed privacy review. No tracking SDKs, minimal permissions, open source.",
|
|
"apps": [
|
|
("com.duckduckgo.mobile.android", "DuckDuckGo"),
|
|
("com.cromite.app", "Cromite"),
|
|
],
|
|
},
|
|
{
|
|
"d": "privacy-approved-social-media",
|
|
"name": "Privacy Approved — Social Media",
|
|
"description": "Federated and alternative social media clients that passed privacy review. No tracking SDKs, minimal permissions, open source.",
|
|
"apps": [
|
|
("org.joinmastodon.android", "Mastodon"),
|
|
("tw.nekomimi.nekogram", "Nekogram"),
|
|
("ml.docilealligator.infinityforreddit.plus", "Infinity+"),
|
|
("com.newsblur", "NewsBlur"),
|
|
("dev.msfjarvis.claw.android", "Claw"),
|
|
("com.streamatico.polymarketviewer", "Polymarket Viewer"),
|
|
],
|
|
},
|
|
{
|
|
"d": "privacy-approved-finance-budgeting",
|
|
"name": "Privacy Approved — Finance & Budgeting",
|
|
"description": "Personal finance, expense tracking, and budgeting apps that passed privacy review. No tracking SDKs, minimal permissions, open source.",
|
|
"apps": [
|
|
("com.flux", "Flow"),
|
|
("com.pennywiseai.tracker", "Pennywise AI Tracker"),
|
|
("com.github.premnirmal.tickerwidget.dev", "Dev Stocks Widget"),
|
|
("com.pearguard", "PearGuard"),
|
|
("network.mostro.app", "Mostro"),
|
|
("eu.domob.shopt2", "Shopt"),
|
|
],
|
|
},
|
|
{
|
|
"d": "privacy-approved-health-fitness",
|
|
"name": "Privacy Approved — Health & Fitness",
|
|
"description": "Health tracking, diet, and fitness apps that passed privacy review. No tracking SDKs, minimal permissions, open source.",
|
|
"visible": True, # Only this stack is visible on the Zapstore app (test stack)
|
|
"apps": [
|
|
("org.nutritionfacts.dailydozen", "Daily Dozen"),
|
|
("com.antoniegil.astronia", "Astronia"),
|
|
],
|
|
},
|
|
{
|
|
"d": "privacy-approved-education-reference",
|
|
"name": "Privacy Approved — Education & Reference",
|
|
"description": "Learning, dictionary, and reference tools that passed privacy review. No tracking SDKs, minimal permissions, open source.",
|
|
"apps": [
|
|
("org.freecodecamp", "freeCodeCamp"),
|
|
("de.felixnuesse.disky", "freeDictionary"),
|
|
("org.eu.mumulhl.ciyue", "Ciyue"),
|
|
("com.tavultesoft.kmapro", "Keyman"),
|
|
("com.hardbacknutter.nevertoomanybooks", "NeverTooManyBooks"),
|
|
("de.doen1el.calibrewebcompanion", "Calibre Web Companion"),
|
|
("io.github.adithya_jayan.myrepertoirapp.fdroid", "Repertoire"),
|
|
("org.listenbrainz.android", "ListenBrainz"),
|
|
("com.kgurgul.cpuinfo", "CPU Info"),
|
|
("com.mkulesh.micromath.plus", "microMathematics Plus"),
|
|
],
|
|
},
|
|
{
|
|
"d": "privacy-approved-photography-image-tools",
|
|
"name": "Privacy Approved — Photography & Image Tools",
|
|
"description": "Camera, photo editing, scanning, and gallery apps that passed privacy review. No tracking SDKs, minimal permissions, open source.",
|
|
"apps": [
|
|
("tech.lolli.toolbox", "Image Toolbox"),
|
|
("org.fairscan.app", "FairScan"),
|
|
("ua.com.radiokot.photoprism", "PhotoPrism"),
|
|
("com.kjxbyz.picguard", "PicGuard"),
|
|
("com.dot.gallery", "Gallery apps"),
|
|
("com.deniscerri.ytdl", "YTDLnis"),
|
|
("me.elcaju", "ElCaju"),
|
|
],
|
|
},
|
|
{
|
|
"d": "privacy-approved-utilities-tools",
|
|
"name": "Privacy Approved — Utilities & Tools",
|
|
"description": "System utilities, converters, and general-purpose tools that passed privacy review. No tracking SDKs, minimal permissions, open source.",
|
|
"apps": [
|
|
("de.markusfisch.android.binaryeye", "Binary Eye"),
|
|
("com.kgurgul.cpuinfo", "CPU Info"),
|
|
("com.ferrarid.converterpro", "Converter NOW"),
|
|
("me.hackerchick.catima", "Catima"),
|
|
("app.lawnchair.lawnicons", "Lawnicons"),
|
|
("app.simple.peri", "Peristyle"),
|
|
("org.exbin.bined.editor.android", "BinEd"),
|
|
("com.dergoogler.mmrl", "MMRL"),
|
|
("io.github.samolego.canta", "Canta"),
|
|
("deltazero.amarok.foss", "Amarok"),
|
|
("com.rama.mako", "Mako"),
|
|
("com.shub39.rush", "Rush"),
|
|
("helium314.keyboard", "HeliBoard"),
|
|
("com.best.deskclock", "DeskClock"),
|
|
("com.acszo.redomi", "Redomi"),
|
|
("com.arn.scrobble", "Scrobble"),
|
|
("com.nononsenseapps.feeder.play", "Feeder"),
|
|
("io.github.sms2email.sms2email", "SMS2Email"),
|
|
("app.simple.inure", "Inure"),
|
|
("com.unciv.app", "Unciv"),
|
|
],
|
|
},
|
|
{
|
|
"d": "privacy-approved-games",
|
|
"name": "Privacy Approved — Games",
|
|
"description": "Open source games across genres that passed privacy review. No tracking SDKs, minimal permissions, open source.",
|
|
"apps": [
|
|
("com.shatteredpixel.shatteredpixeldungeon", "Shattered Pixel Dungeon"),
|
|
("org.wesnoth.wesnoth", "Wesnoth"),
|
|
("de.sesu8642.feudaltactics", "Feudal Tactics"),
|
|
("com.agateau.burgerparty", "Burger Party"),
|
|
("de.chadenas.cpudefense", "ChipDefense"),
|
|
("de.z11.roboyard", "Roboyard"),
|
|
("com.damasclash", "Damas Clash"),
|
|
("com.unciv.app", "Unciv"),
|
|
],
|
|
},
|
|
{
|
|
"d": "privacy-approved-communication-non-nostr",
|
|
"name": "Privacy Approved — Communication (Non-Nostr)",
|
|
"description": "Voice/video calls, remote desktop, and messaging apps that passed privacy review. No tracking SDKs, minimal permissions, open source.",
|
|
"apps": [
|
|
("org.telegram.messenger.web", "Telegram"),
|
|
("com.carriez.flutter_hbb", "RustDesk"),
|
|
("io.unsigned.sideband", "Sideband"),
|
|
("com.geeksville.mesh", "Meshtastic"),
|
|
],
|
|
},
|
|
{
|
|
"d": "privacy-approved-development-tools",
|
|
"name": "Privacy Approved — Development Tools",
|
|
"description": "Code editors, Git clients, and developer tools that passed privacy review. No tracking SDKs, minimal permissions, open source.",
|
|
"apps": [
|
|
("com.foxdebug.acode", "Acode"),
|
|
("com.viscouspot.gitsync", "GitSync"),
|
|
("org.exbin.bined.editor.android", "BinEd"),
|
|
("org.freecodecamp", "freeCodeCamp"),
|
|
("zed.rainxch.githubstore", "GitHub Store"),
|
|
("com.inspiredandroid.kai", "Kai 9000"),
|
|
],
|
|
},
|
|
{
|
|
"d": "privacy-approved-calendar-scheduling",
|
|
"name": "Privacy Approved — Calendar & Scheduling",
|
|
"description": "Calendar apps, scheduling, and time management tools that passed privacy review. No tracking SDKs, minimal permissions, open source.",
|
|
"apps": [
|
|
("app.formstr.calendar", "Calendar by Form*"),
|
|
("com.pearcal", "PearCal"),
|
|
("me.timeto.app", "timeto.me"),
|
|
("com.blankdev.sidestep", "Sidestep"),
|
|
],
|
|
},
|
|
]
|
|
|
|
# ── Gitea mirror URL lookup ────────────────────────────────────────
|
|
|
|
def gitea_url(identifier):
|
|
return f"https://laantungir.net/git/zapstore-mirror/{identifier}"
|
|
|
|
|
|
# ── n_signer qrexec helpers ────────────────────────────────────────
|
|
|
|
def qrexec_call(request):
|
|
framed = struct.pack(">I", len(json.dumps(request).encode())) + json.dumps(request).encode()
|
|
proc = subprocess.run(
|
|
["qrexec-client-vm", QREXEC_TARGET, QREXEC_SERVICE],
|
|
input=framed, capture_output=True, timeout=30
|
|
)
|
|
if proc.returncode != 0:
|
|
raise Exception(f"qrexec failed: {proc.stderr.decode()}")
|
|
resp_len = struct.unpack(">I", proc.stdout[:4])[0]
|
|
return json.loads(proc.stdout[4:4 + resp_len])
|
|
|
|
|
|
# ── Relay publish helper ───────────────────────────────────────────
|
|
|
|
async def publish_event(signed_event, label=""):
|
|
async def publish_one(relay):
|
|
try:
|
|
async with websockets.connect(relay, max_size=10_000_000, open_timeout=10) as ws:
|
|
await ws.send(json.dumps(["EVENT", signed_event]))
|
|
while True:
|
|
resp = await asyncio.wait_for(ws.recv(), timeout=10)
|
|
data = json.loads(resp)
|
|
if data[0] == "OK" and data[1] == signed_event["id"]:
|
|
return relay, data[2]
|
|
except Exception:
|
|
return relay, False
|
|
|
|
results = await asyncio.gather(*[publish_one(r) for r in RELAYS])
|
|
ok = sum(1 for _, a in results if a)
|
|
total = len(results)
|
|
print(f" {label}Published to {ok}/{total} relays")
|
|
return ok > 0
|
|
|
|
|
|
# ── Main ───────────────────────────────────────────────────────────
|
|
|
|
async def main():
|
|
print("=" * 60)
|
|
print("Publishing all app stacks and app definitions")
|
|
print("=" * 60)
|
|
|
|
# Get pubkey
|
|
print("\n1. Getting pubkey from n_signer...")
|
|
req = {"id": "1", "method": "nostr_get_public_key", "params": [{"nostr_index": 0}]}
|
|
resp = qrexec_call(req)
|
|
if "error" in resp:
|
|
print(f" Error: {resp['error']}")
|
|
sys.exit(1)
|
|
pubkey = resp["result"]
|
|
print(f" Pubkey: {pubkey}")
|
|
|
|
results = {"stacks": 0, "stacks_failed": 0, "apps": 0, "apps_failed": 0}
|
|
|
|
# ── Publish all stacks ──────────────────────────────────────────
|
|
print(f"\n2. Publishing {len(STACKS)} app stacks...")
|
|
for i, stack in enumerate(STACKS, 1):
|
|
created_at = int(time.time())
|
|
# Only stacks with the community pubkey in the `h` tag appear in the
|
|
# Zapstore app. Set "visible": True in the stack dict to make it visible.
|
|
h_tag = KZAPSTORE_COMMUNITY_PUBKEY if stack.get("visible") else ""
|
|
tags = [
|
|
["d", stack["d"]],
|
|
["t", "app-stack"],
|
|
["name", stack["name"]],
|
|
["description", stack["description"]],
|
|
["f", "android-arm64-v8a"],
|
|
["h", h_tag],
|
|
]
|
|
# Add a tag for each app (kind 32267 Software Application references)
|
|
# relay.zapstore.dev validates that a-tags in kind 30267 events reference kind 32267.
|
|
# Use real app developer pubkeys from app_pubkeys.json where available;
|
|
# skip apps that don't have a kind 32267 event on the relay.
|
|
included = 0
|
|
skipped = 0
|
|
for app_id, app_name in stack["apps"]:
|
|
real_pubkey = APP_PUBKEYS.get(app_id)
|
|
if real_pubkey:
|
|
tags.append(["a", f"32267:{real_pubkey}:{app_id}", "wss://relay.zapstore.dev"])
|
|
included += 1
|
|
else:
|
|
skipped += 1
|
|
if skipped:
|
|
print(f" [{i}/{len(STACKS)}] {stack['name']}: {included} apps included, {skipped} skipped (no kind 32267 event)")
|
|
|
|
event = {
|
|
"kind": 30267,
|
|
"content": "", # Must be empty — zapstore treats non-empty content as encrypted
|
|
"tags": tags,
|
|
"created_at": created_at,
|
|
"pubkey": pubkey,
|
|
}
|
|
|
|
req = {"id": "2", "method": "nostr_sign_event", "params": [json.dumps(event), {"nostr_index": 0}]}
|
|
resp = qrexec_call(req)
|
|
if "error" in resp:
|
|
print(f" [{i}/{len(STACKS)}] ❌ {stack['name']}: {resp['error']}")
|
|
results["stacks_failed"] += 1
|
|
continue
|
|
|
|
signed = json.loads(resp["result"])
|
|
print(f" [{i}/{len(STACKS)}] {stack['name']} ({len(stack['apps'])} apps) id={signed['id'][:16]}...")
|
|
await publish_event(signed, " ")
|
|
results["stacks"] += 1
|
|
|
|
# ── Publish all app definitions ─────────────────────────────────
|
|
total_apps = sum(len(s["apps"]) for s in STACKS)
|
|
print(f"\n3. Publishing {total_apps} app definitions...")
|
|
|
|
app_num = 0
|
|
for stack in STACKS:
|
|
for app_id, app_name in stack["apps"]:
|
|
app_num += 1
|
|
repo_url = gitea_url(app_id)
|
|
created_at = int(time.time())
|
|
|
|
event = {
|
|
"kind": 30078,
|
|
"content": json.dumps({
|
|
"name": app_name,
|
|
"identifier": app_id,
|
|
"repository": repo_url,
|
|
"description": "",
|
|
}),
|
|
"tags": [
|
|
["d", "app-" + app_id],
|
|
["t", "app-definition"],
|
|
["t", stack["d"]],
|
|
["name", app_name],
|
|
["repository", repo_url],
|
|
["gitea", app_id],
|
|
],
|
|
"created_at": created_at,
|
|
"pubkey": pubkey,
|
|
}
|
|
|
|
req = {"id": "3", "method": "nostr_sign_event", "params": [json.dumps(event), {"nostr_index": 0}]}
|
|
resp = qrexec_call(req)
|
|
if "error" in resp:
|
|
print(f" [{app_num}/{total_apps}] ❌ {app_name}: {resp['error']}")
|
|
results["apps_failed"] += 1
|
|
continue
|
|
|
|
signed = json.loads(resp["result"])
|
|
print(f" [{app_num}/{total_apps}] {app_name} ({app_id}) id={signed['id'][:16]}...")
|
|
await publish_event(signed, " ")
|
|
results["apps"] += 1
|
|
|
|
# ── Summary ─────────────────────────────────────────────────────
|
|
print("\n" + "=" * 60)
|
|
print("SUMMARY")
|
|
print("=" * 60)
|
|
print(f" Stacks: {results['stacks']} published, {results['stacks_failed']} failed")
|
|
print(f" Apps: {results['apps']} published, {results['apps_failed']} failed")
|
|
print(f" Total: {results['stacks'] + results['apps']} events")
|
|
print("=" * 60)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|