- ChatBubble: useActionSheet replaces custom Modal (native iOS popup, Android bottom sheet) - DM mode (isDM prop): hides like-count, shows Insta-style heart badge under bubble when liked - Group chat unchanged - Cleanup: remove unused Modal/Platform imports, sheet styles, actionsOpen state - deploy.sh: auto-detect ANDROID_HOME + auto-create local.properties for local Gradle - NEXT_RELEASE.md: DM reactions release note - Includes other staged work across binder-mac, marketing, ops/mdm, ios/
114 lines
4.1 KiB
Python
114 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
ReBreak Unsupervised Sideload Profile Generator
|
|
================================================
|
|
Generiert pro-User .mobileconfig mit frischen UUIDs + individuellem
|
|
RemovalPassword. Validiert das Resultat via `plutil -lint`.
|
|
|
|
KEIN Signing-Step (User-Entscheidung 2026-05-26: unsigned ausliefern).
|
|
iOS zeigt "Nicht überprüft" rot beim Install, akzeptabel für DiGA-Pilot.
|
|
|
|
Usage:
|
|
python3 generate-unsupervised-profile.py \\
|
|
--removal-password 482915 \\
|
|
--org "ReBreak GmbH" \\
|
|
--output /tmp/rebreak-schutz-user-abc123.mobileconfig
|
|
|
|
python3 generate-unsupervised-profile.py --batch users.csv --output-dir /tmp/profiles/
|
|
|
|
CSV-Format für --batch:
|
|
user_id,removal_password
|
|
abc123,482915
|
|
def456,719204
|
|
"""
|
|
|
|
import argparse
|
|
import csv
|
|
import plistlib
|
|
import subprocess
|
|
import sys
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
# Template-Quelldatei — wird gelesen, UUIDs + RemovalPassword werden ersetzt.
|
|
TEMPLATE_PATH = Path(__file__).parent / "rebreak-iphone-unsupervised-sideload.mobileconfig"
|
|
|
|
|
|
def generate_profile(removal_password: str, org_name: str = "ReBreak") -> bytes:
|
|
"""
|
|
Lädt das Template, generiert frische UUIDs für alle PayloadUUID-Felder,
|
|
ersetzt RemovalPassword + PayloadOrganization, returnt finale plist als bytes.
|
|
"""
|
|
with open(TEMPLATE_PATH, "rb") as f:
|
|
plist = plistlib.load(f)
|
|
|
|
# Top-Level
|
|
plist["PayloadUUID"] = str(uuid.uuid4()).upper()
|
|
plist["PayloadOrganization"] = org_name
|
|
plist["RemovalPassword"] = removal_password
|
|
|
|
# Sub-Payloads: jeder kriegt eine frische UUID
|
|
for sub in plist.get("PayloadContent", []):
|
|
sub["PayloadUUID"] = str(uuid.uuid4()).upper()
|
|
|
|
return plistlib.dumps(plist, fmt=plistlib.FMT_XML)
|
|
|
|
|
|
def validate_plist(profile_bytes: bytes, path: Path) -> None:
|
|
"""
|
|
Validiert via macOS plutil -lint. Schreibt temporär, lintet, löscht.
|
|
Wirft Exception bei Lint-Fehlern.
|
|
"""
|
|
path.write_bytes(profile_bytes)
|
|
result = subprocess.run(
|
|
["plutil", "-lint", str(path)],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0:
|
|
raise RuntimeError(f"plutil -lint failed: {result.stdout} {result.stderr}")
|
|
|
|
|
|
def write_profile(removal_password: str, org_name: str, output_path: Path) -> None:
|
|
profile = generate_profile(removal_password=removal_password, org_name=org_name)
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
validate_plist(profile, output_path)
|
|
print(f"✓ {output_path} (removal-PIN: {removal_password})")
|
|
|
|
|
|
def batch_from_csv(csv_path: Path, output_dir: Path, org_name: str) -> None:
|
|
with open(csv_path) as f:
|
|
reader = csv.DictReader(f)
|
|
for row in reader:
|
|
user_id = row["user_id"].strip()
|
|
removal_pin = row["removal_password"].strip()
|
|
output_path = output_dir / f"rebreak-schutz-{user_id}.mobileconfig"
|
|
write_profile(removal_pin, org_name, output_path)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument("--removal-password", help="6+ digit PIN für Profil-Removal (einzelner User)")
|
|
parser.add_argument("--org", default="ReBreak", help="PayloadOrganization-Name")
|
|
parser.add_argument("--output", help="Output-Pfad (einzelner User)")
|
|
parser.add_argument("--batch", help="CSV-Datei: user_id,removal_password")
|
|
parser.add_argument("--output-dir", help="Output-Verzeichnis für --batch")
|
|
args = parser.parse_args()
|
|
|
|
if args.batch:
|
|
if not args.output_dir:
|
|
parser.error("--batch braucht --output-dir")
|
|
batch_from_csv(Path(args.batch), Path(args.output_dir), args.org)
|
|
else:
|
|
if not (args.removal_password and args.output):
|
|
parser.error("--removal-password und --output sind required (oder --batch nutzen)")
|
|
if not args.removal_password.isdigit() or len(args.removal_password) < 6:
|
|
parser.error("--removal-password muss 6+ Ziffern haben")
|
|
write_profile(args.removal_password, args.org, Path(args.output))
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|