#!/usr/bin/env python3 import json import time from pathlib import Path import requests INSTANCE = "https://rheinneckar.events" # Wichtig: # "read" wird benötigt, damit loggedUser.memberships gelesen werden darf. # Die write:event:* Scopes werden später für das Erstellen/Aktualisieren/Löschen von Events genutzt. SCOPES = "read write:event:create write:event:update write:event:delete" APP_FILE = Path("mobilizon-app.json") TOKEN_FILE = Path("mobilizon-token.json") ENV_FILE = Path(".env") def post_form(url, data): response = requests.post(url, data=data, timeout=30) print(f"POST {url} -> HTTP {response.status_code}") try: payload = response.json() except Exception: print("Keine JSON-Antwort:") print(response.text) raise SystemExit(1) if response.status_code >= 400: print("Fehlerantwort:") print(json.dumps(payload, indent=2, ensure_ascii=False)) raise SystemExit(1) return payload def graphql(access_token, query, variables=None): response = requests.post( f"{INSTANCE}/api", headers={ "Authorization": f"Bearer {access_token}", "Content-Type": "application/json", }, json={ "query": query, "variables": variables or {}, }, timeout=30, ) print(f"GraphQL -> HTTP {response.status_code}") try: payload = response.json() except Exception: print("Keine JSON-Antwort:") print(response.text) raise SystemExit(1) if response.status_code >= 400 or payload.get("errors"): print("GraphQL-Fehler:") print(json.dumps(payload, indent=2, ensure_ascii=False)) raise SystemExit(1) return payload["data"] def register_app(): print("1. App wird registriert...") app = post_form( f"{INSTANCE}/apps", { "name": "OSMCal Importer", "redirect_uri": "urn:ietf:wg:oauth:2.0:oob", "website": INSTANCE, "scope": SCOPES, }, ) APP_FILE.write_text( json.dumps(app, indent=2, ensure_ascii=False), encoding="utf-8", ) return app def request_device_code(client_id): print() print("2. Login-Code wird angefordert...") device = post_form( f"{INSTANCE}/login/device/code", { "client_id": client_id, "scope": SCOPES, }, ) print() print("Öffne diese Adresse im Browser:") print(device["verification_uri"]) print() print("Gib dort diesen Code ein:") print(device["user_code"]) print() print("Danach hier warten. Das Script fragt automatisch ab, ob du bestätigt hast.") print() return device def wait_for_token(client_id, device): interval = int(device.get("interval", 5)) expires_in = int(device.get("expires_in", 900)) deadline = time.time() + expires_in while time.time() < deadline: time.sleep(interval) response = requests.post( f"{INSTANCE}/oauth/token", data={ "client_id": client_id, "device_code": device["device_code"], "grant_type": "urn:ietf:params:oauth:grant-type:device_code", }, timeout=30, ) try: payload = response.json() except Exception: print("Keine JSON-Antwort beim Token-Abruf:") print(response.text) raise SystemExit(1) if "access_token" in payload: print("Token erhalten.") TOKEN_FILE.write_text( json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8", ) return payload error = payload.get("error") if error == "authorization_pending": print("Noch nicht bestätigt...") continue if error == "slow_down": print("Server bittet um langsameres Abfragen...") interval += 5 continue print("Token-Abruf fehlgeschlagen:") print(json.dumps(payload, indent=2, ensure_ascii=False)) raise SystemExit(1) print("Der Login-Code ist abgelaufen. Script neu starten.") raise SystemExit(1) def get_memberships(access_token): print() print("3. Gruppenmitgliedschaften werden abgefragt...") query = """ query LoggedUserMemberships($page: Int, $limit: Int) { loggedUser { id memberships(page: $page, limit: $limit) { total elements { role actor { id type preferredUsername name } parent { id type preferredUsername name } } } } } """ data = graphql( access_token, query, variables={ "page": 1, "limit": 100, }, ) memberships_data = data.get("loggedUser", {}).get("memberships") if not memberships_data: print() print("Keine Gruppenmitgliedschaften gefunden.") print("Mögliche Ursachen:") print("- Du bist mit diesem Account in keiner Gruppe Mitglied.") print("- Die App wurde nicht mit dem Scope 'read' autorisiert.") print("- Der verwendete Account ist nicht der erwartete Mobilizon-Account.") raise SystemExit(1) memberships = memberships_data.get("elements", []) if not memberships: print() print("Du bist mit diesem Account in keiner Gruppe Mitglied.") raise SystemExit(1) return memberships def choose_membership(memberships): print() print("Deine Gruppen:") print() for index, membership in enumerate(memberships, start=1): profile = membership["actor"] group = membership["parent"] role = membership["role"] profile_name = profile.get("name") or profile.get("preferredUsername") or "Profil" group_name = group.get("name") or group.get("preferredUsername") or "Gruppe" print( f"{index}. Gruppe: {group_name} " f"(group_id={group['id']}) | " f"Profil: {profile_name} " f"(profile_id={profile['id']}) | " f"Rolle: {role}" ) print() choice = input("Welche Gruppe soll für den Import verwendet werden? Nummer eingeben: ").strip() try: selected = memberships[int(choice) - 1] except Exception: print("Ungültige Auswahl.") raise SystemExit(1) role = selected["role"] if role not in ("MEMBER", "MODERATOR", "ADMINISTRATOR"): print() print(f"Warnung: Deine Rolle ist {role}.") print("Falls der Import später fehlschlägt, darf dieses Profil vermutlich keine Gruppen-Events veröffentlichen.") return selected def write_env(app, token, membership): organizer_actor_id = membership["actor"]["id"] attributed_to_id = membership["parent"]["id"] lines = [ f'MOBILIZON_INSTANCE="{INSTANCE}"', f'MOBILIZON_CLIENT_ID="{app["client_id"]}"', f'MOBILIZON_CLIENT_SECRET="{app.get("client_secret", "")}"', f'MOBILIZON_TOKEN="{token["access_token"]}"', f'MOBILIZON_REFRESH_TOKEN="{token.get("refresh_token", "")}"', f'MOBILIZON_ORGANIZER_ACTOR_ID="{organizer_actor_id}"', f'MOBILIZON_ATTRIBUTED_TO_ID="{attributed_to_id}"', ] ENV_FILE.write_text("\n".join(lines) + "\n", encoding="utf-8") ENV_FILE.chmod(0o600) print() print("Fertig. .env wurde geschrieben.") print() print("Gesetzte Werte:") print(f"MOBILIZON_ORGANIZER_ACTOR_ID={organizer_actor_id}") print(f"MOBILIZON_ATTRIBUTED_TO_ID={attributed_to_id}") print() print("Hinweis: Der Access-Token läuft nach einigen Stunden ab.") print("Für erste Tests reicht das. Für Cron-Betrieb sollte später Refresh-Token-Unterstützung ergänzt werden.") def main(): app = register_app() device = request_device_code(app["client_id"]) token = wait_for_token(app["client_id"], device) memberships = get_memberships(token["access_token"]) selected = choose_membership(memberships) write_env(app, token, selected) if __name__ == "__main__": main()