From 8513224a0499678dd7f0e76b4ee2b36dd26f4c8d Mon Sep 17 00:00:00 2001 From: Thies Mueller Date: Fri, 12 Jun 2026 17:08:19 +0200 Subject: [PATCH] complete p3.14 rewrite --- app.py | 376 ++++++++++++++++++++++++++++++----------------- requirements.txt | 8 +- 2 files changed, 243 insertions(+), 141 deletions(-) diff --git a/app.py b/app.py index 73ab439..1e3193b 100644 --- a/app.py +++ b/app.py @@ -1,174 +1,204 @@ -import asyncio +import time +import jwt import aiohttp +import asyncio import configparser -from apns2.client import APNsClient -from apns2.payload import Payload -from apns2.credentials import TokenCredentials + from flask import Flask, request, jsonify from flask_sqlalchemy import SQLAlchemy -from datetime import datetime + config = configparser.ConfigParser() -config.read('config.ini') +config.read("config.ini") -REGISTER_AUTH = config.get('AUTH', 'REGISTER_AUTH', fallback=None) -MANAGE_AUTH = config.get('AUTH', 'MANAGE_AUTH', fallback=None) -SQLALCHEMY_DATABASE_URI = config.get('DATABASE', 'SQLALCHEMY_DATABASE_URI', fallback='sqlite:///device_tokens.db') -auth_key_path = config.get('APNS', 'auth_key_path', fallback='./APNSAuthKey.p8') -auth_key_id = config.get('APNS', 'auth_key_id', fallback=None) -team_id = config.get('APNS', 'team_id', fallback=None) -topic = config.get('APNS', 'topic', fallback=None) -results_api = config.get('API', 'resultsapi', fallback='https://sat-api.tservic.es/api/v1/results') +REGISTER_AUTH = config.get("AUTH", "REGISTER_AUTH", fallback=None) +MANAGE_AUTH = config.get("AUTH", "MANAGE_AUTH", fallback=None) +SQLALCHEMY_DATABASE_URI = config.get( + "DATABASE", + "SQLALCHEMY_DATABASE_URI", + fallback="sqlite:///device_tokens.db" +) -if not all([REGISTER_AUTH, MANAGE_AUTH, auth_key_id, team_id, topic, results_api]): +auth_key_path = config.get( + "APNS", + "auth_key_path", + fallback="./APNSAuthKey.p8" +) + +auth_key_id = config.get("APNS", "auth_key_id", fallback=None) +team_id = config.get("APNS", "team_id", fallback=None) +topic = config.get("APNS", "topic", fallback=None) + +APNS_URL = "https://api.push.apple.com" + +if not all([ + REGISTER_AUTH, + MANAGE_AUTH, + auth_key_id, + team_id, + topic +]): raise ValueError("Fehlende Pflichtfelder in der Konfiguration!") app = Flask(__name__) -app.config['SQLALCHEMY_DATABASE_URI'] = SQLALCHEMY_DATABASE_URI + +app.config["SQLALCHEMY_DATABASE_URI"] = SQLALCHEMY_DATABASE_URI +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + db = SQLAlchemy(app) -def authenticate(request, required_token): - auth_header = request.headers.get("Authorization") - return auth_header == f"Bearer {required_token}" - - class DeviceToken(db.Model): id = db.Column(db.Integer, primary_key=True) - device_token = db.Column(db.String(256), nullable=False, unique=True) -class NotificationLog(db.Model): - id = db.Column(db.Integer, primary_key=True) - result_uuid = db.Column(db.String(256), nullable=False, unique=True) - sent_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) - - -def create_apns_client(): - token_credentials = TokenCredentials( - auth_key_path=auth_key_path, - auth_key_id=auth_key_id, - team_id=team_id + device_token = db.Column( + db.String(256), + nullable=False, + unique=True ) - return APNsClient(credentials=token_credentials, use_sandbox=False) -def send_notification(device_token, title, subtitle, text, uuid=None): - try: - apns_client = create_apns_client() - custom_data = { - "subtitle": subtitle, - "text": text +def authenticate(req, required_token): + auth_header = req.headers.get("Authorization") + return auth_header == f"Bearer {required_token}" + +with open(auth_key_path, "r") as f: + APNS_PRIVATE_KEY = f.read() + + +def generate_apns_token(): + return jwt.encode( + { + "iss": team_id, + "iat": int(time.time()) + }, + APNS_PRIVATE_KEY, + algorithm="ES256", + headers={ + "alg": "ES256", + "kid": auth_key_id } - if uuid: - custom_data["uuid"] = uuid + ) - payload = Payload( - alert={"title": title, "body": text}, - sound="default", - badge=1, - category="RACE_RESULT", - custom=custom_data - ) - apns_client.send_notification(device_token, payload, topic) - except Exception as e: - print(f"Fehler beim Senden der Benachrichtigung: {e}") - -async def fetch_results_and_send_notifications(): +async def send_notification( + session, + device_token, + title, + body +): try: - async with aiohttp.ClientSession() as session: - async with session.get(results_api) as response: - if response.status != 200: - raise Exception(f"API Fehler: {response.status}") - results = await response.json() - results = results.get('ergebnisse', {}) - if not results: - raise ValueError("Keine Ergebnisse gefunden.") - device_tokens = DeviceToken.query.all() - for result in results.values(): - winner_bahn = result[result['winner']] - title = f"{result['title']} Ergebnis: {winner_bahn['boot']} gewinnt mit {winner_bahn['zeit']}" - subtitle = f"{winner_bahn['boot']} gewinnt mit {winner_bahn['zeit']}" - text = "Klicke hier, um die Ergebnisse anzusehen" + jwt_token = generate_apns_token() - if NotificationLog.query.filter_by(result_uuid=result['uuid']).first(): - continue + payload = { + "aps": { + "alert": { + "title": title, + "body": body + }, + "sound": "default", + "badge": 1 + } + } - for token in device_tokens: - send_notification(token.device_token, title, subtitle, text) + headers = { + "authorization": f"bearer {jwt_token}", + "apns-topic": topic, + "apns-push-type": "alert", + "content-type": "application/json" + } + + url = f"{APNS_URL}/3/device/{device_token}" + + async with session.post( + url, + json=payload, + headers=headers + ) as response: + + if response.status != 200: + + error_text = await response.text() + + print( + f"APNS Fehler {response.status} " + f"für {device_token}: {error_text}" + ) - new_log = NotificationLog(result_uuid=result['uuid']) - db.session.add(new_log) - db.session.commit() except Exception as e: - print(f"Fehler beim Abrufen und Senden von Benachrichtigungen: {e}") - -@app.route("/api/getresults", methods=["POST"]) -def get_results(): - if not authenticate(request, MANAGE_AUTH): - return jsonify({"error": "Unauthorized"}), 401 - try: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - loop.run_until_complete(fetch_results_and_send_notifications()) - return jsonify({"message": "Benachrichtigungen gesendet"}), 200 - except Exception as e: - return jsonify({'error': 'Interner Serverfehler', 'details': str(e)}), 500 + print(f"Fehler beim Senden: {e}") @app.route("/api/registerDeviceToken", methods=["POST"]) def register_device_token(): + if not authenticate(request, REGISTER_AUTH): - return jsonify({"error": "Unauthorized"}), 401 + return jsonify({ + "error": "Unauthorized" + }), 401 + try: + data = request.get_json() - if 'device_token' not in data: - return jsonify({'error': 'device_token fehlt'}), 400 - new_device_token = DeviceToken(device_token=data['device_token']) - db.session.add(new_device_token) + if not data or "device_token" not in data: + return jsonify({ + "error": "device_token fehlt" + }), 400 + + device_token = data["device_token"] + + existing = DeviceToken.query.filter_by( + device_token=device_token + ).first() + + if existing: + return jsonify({ + "message": "Bereits registriert" + }), 200 + + db.session.add( + DeviceToken(device_token=device_token) + ) + db.session.commit() - return jsonify({'message': 'Device Token erfolgreich registriert'}), 200 + + return jsonify({ + "message": "Device Token registriert" + }), 200 + except Exception as e: - return jsonify({'error': 'Interner Serverfehler', 'details': str(e)}), 500 -@app.route("/showdevicetokens", methods=["GET"]) -def show_device_tokens(): - tokens = DeviceToken.query.all() - return {"device_tokens": [token.device_token for token in tokens]} + db.session.rollback() -@app.route("/shownotificationlog", methods=["GET"]) -def show_notification_log(): - logs = NotificationLog.query.all() - log_data = [{ - 'result_uuid': log.result_uuid, - 'sent_at': log.sent_at.strftime('%Y-%m-%d %H:%M:%S') - } for log in logs] - return jsonify({'notification_logs': log_data}), 200 + return jsonify({ + "error": "Interner Serverfehler", + "details": str(e) + }), 500 + + +@app.route("/api/notify", methods=["POST"]) +def notify(): -@app.route("/api/deleteAllDeviceTokens", methods=["POST"]) -def delete_all_device_tokens(): if not authenticate(request, MANAGE_AUTH): - return jsonify({"error": "Unauthorized"}), 401 - try: - db.session.query(DeviceToken).delete() - db.session.commit() - return jsonify({'message': 'Alle Device Tokens wurden gelöscht'}), 200 - except Exception as e: - return jsonify({'error': 'Interner Serverfehler', 'details': str(e)}), 500 - -@app.route("/api/customnotify", methods=["POST"]) -def custom_notify(): - if not authenticate(request, MANAGE_AUTH): - return jsonify({"error": "Unauthorized"}), 401 + return jsonify({ + "error": "Unauthorized" + }), 401 try: + data = request.get_json() + severity = data.get("severity") notification_text = data.get("notification") if not severity or not notification_text: - return jsonify({"error": "severity und notification sind erforderlich"}), 400 + return jsonify({ + "error": ( + "severity und notification " + "sind erforderlich" + ) + }), 400 severity_icons = { "info": "", @@ -178,28 +208,98 @@ def custom_notify(): } if severity not in severity_icons: - return jsonify({"error": "Ungültige severity"}), 400 + return jsonify({ + "error": "Ungültige severity" + }), 400 - title = f"{severity_icons[severity]}{notification_text}" - subtitle = "ignore" - text = "ignore" + title = ( + f"{severity_icons[severity]}" + f"{notification_text}" + ) - device_tokens = DeviceToken.query.all() - for token in device_tokens: - send_notification( - token.device_token, - title=title, - subtitle=subtitle, - text=text, - uuid=result['uuid'] - ) + async def send_all(): + + async with aiohttp.ClientSession() as session: + + tasks = [] + + for token in DeviceToken.query.all(): + + tasks.append( + send_notification( + session=session, + device_token=token.device_token, + title=title, + body=notification_text + ) + ) + + await asyncio.gather(*tasks) + + asyncio.run(send_all()) + + return jsonify({ + "message": "Benachrichtigungen gesendet" + }), 200 - return jsonify({"message": "Benachrichtigungen gesendet"}), 200 except Exception as e: - return jsonify({'error': 'Interner Serverfehler', 'details': str(e)}), 500 + + return jsonify({ + "error": "Interner Serverfehler", + "details": str(e) + }), 500 + + +@app.route("/showdevicetokens", methods=["GET"]) +def show_device_tokens(): + + tokens = DeviceToken.query.all() + + return jsonify({ + "device_tokens": [ + token.device_token + for token in tokens + ] + }) + + +@app.route( + "/api/deleteAllDeviceTokens", + methods=["POST"] +) +def delete_all_device_tokens(): + + if not authenticate(request, MANAGE_AUTH): + return jsonify({ + "error": "Unauthorized" + }), 401 + + try: + + db.session.query(DeviceToken).delete() + + db.session.commit() + + return jsonify({ + "message": "Alle Device Tokens gelöscht" + }), 200 + + except Exception as e: + + db.session.rollback() + + return jsonify({ + "error": "Interner Serverfehler", + "details": str(e) + }), 500 if __name__ == "__main__": + with app.app_context(): db.create_all() - app.run(host='0.0.0.0', port=3000) + + app.run( + host="0.0.0.0", + port=3000 + ) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index ab01d3e..e7aead7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,8 @@ -flask -flask_sqlalchemy asyncio -aiohttp apns2 configparser +flask +flask_sqlalchemy +aiohttp +pyjwt +cryptography \ No newline at end of file