complete p3.14 rewrite

This commit is contained in:
Thies Mueller
2026-06-12 17:08:19 +02:00
parent db270a4e07
commit 8513224a04
2 changed files with 243 additions and 141 deletions
+237 -137
View File
@@ -1,174 +1,204 @@
import asyncio import time
import jwt
import aiohttp import aiohttp
import asyncio
import configparser 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 import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
config = configparser.ConfigParser() config = configparser.ConfigParser()
config.read('config.ini') config.read("config.ini")
REGISTER_AUTH = config.get('AUTH', 'REGISTER_AUTH', fallback=None) REGISTER_AUTH = config.get("AUTH", "REGISTER_AUTH", fallback=None)
MANAGE_AUTH = config.get('AUTH', 'MANAGE_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')
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!") raise ValueError("Fehlende Pflichtfelder in der Konfiguration!")
app = Flask(__name__) 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) 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): class DeviceToken(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
device_token = db.Column(db.String(256), nullable=False, unique=True)
class NotificationLog(db.Model): device_token = db.Column(
id = db.Column(db.Integer, primary_key=True) db.String(256),
result_uuid = db.Column(db.String(256), nullable=False, unique=True) nullable=False,
sent_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) unique=True
def create_apns_client():
token_credentials = TokenCredentials(
auth_key_path=auth_key_path,
auth_key_id=auth_key_id,
team_id=team_id
) )
return APNsClient(credentials=token_credentials, use_sandbox=False)
def send_notification(device_token, title, subtitle, text, uuid=None): def authenticate(req, required_token):
try: auth_header = req.headers.get("Authorization")
apns_client = create_apns_client() return auth_header == f"Bearer {required_token}"
custom_data = {
"subtitle": subtitle, with open(auth_key_path, "r") as f:
"text": text 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: try:
async with aiohttp.ClientSession() as session:
async with session.get(results_api) as response: jwt_token = generate_apns_token()
payload = {
"aps": {
"alert": {
"title": title,
"body": body
},
"sound": "default",
"badge": 1
}
}
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: 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() error_text = await response.text()
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"
if NotificationLog.query.filter_by(result_uuid=result['uuid']).first(): print(
continue f"APNS Fehler {response.status} "
f"für {device_token}: {error_text}"
)
for token in device_tokens:
send_notification(token.device_token, title, subtitle, text)
new_log = NotificationLog(result_uuid=result['uuid'])
db.session.add(new_log)
db.session.commit()
except Exception as e: except Exception as e:
print(f"Fehler beim Abrufen und Senden von Benachrichtigungen: {e}") print(f"Fehler beim Senden: {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
@app.route("/api/registerDeviceToken", methods=["POST"]) @app.route("/api/registerDeviceToken", methods=["POST"])
def register_device_token(): def register_device_token():
if not authenticate(request, REGISTER_AUTH): if not authenticate(request, REGISTER_AUTH):
return jsonify({"error": "Unauthorized"}), 401 return jsonify({
"error": "Unauthorized"
}), 401
try: try:
data = request.get_json() 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']) if not data or "device_token" not in data:
db.session.add(new_device_token) 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() db.session.commit()
return jsonify({'message': 'Device Token erfolgreich registriert'}), 200
return jsonify({
"message": "Device Token registriert"
}), 200
except Exception as e: except Exception as e:
return jsonify({'error': 'Interner Serverfehler', 'details': str(e)}), 500
@app.route("/showdevicetokens", methods=["GET"]) db.session.rollback()
def show_device_tokens():
tokens = DeviceToken.query.all()
return {"device_tokens": [token.device_token for token in tokens]}
@app.route("/shownotificationlog", methods=["GET"]) return jsonify({
def show_notification_log(): "error": "Interner Serverfehler",
logs = NotificationLog.query.all() "details": str(e)
log_data = [{ }), 500
'result_uuid': log.result_uuid,
'sent_at': log.sent_at.strftime('%Y-%m-%d %H:%M:%S')
} for log in logs] @app.route("/api/notify", methods=["POST"])
return jsonify({'notification_logs': log_data}), 200 def notify():
@app.route("/api/deleteAllDeviceTokens", methods=["POST"])
def delete_all_device_tokens():
if not authenticate(request, MANAGE_AUTH): if not authenticate(request, MANAGE_AUTH):
return jsonify({"error": "Unauthorized"}), 401 return jsonify({
try: "error": "Unauthorized"
db.session.query(DeviceToken).delete() }), 401
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
try: try:
data = request.get_json() data = request.get_json()
severity = data.get("severity") severity = data.get("severity")
notification_text = data.get("notification") notification_text = data.get("notification")
if not severity or not notification_text: 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 = { severity_icons = {
"info": "", "info": "",
@@ -178,28 +208,98 @@ def custom_notify():
} }
if severity not in severity_icons: 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}" title = (
subtitle = "ignore" f"{severity_icons[severity]}"
text = "ignore" 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']
) )
return jsonify({"message": "Benachrichtigungen gesendet"}), 200 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
except Exception as e: 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__": if __name__ == "__main__":
with app.app_context(): with app.app_context():
db.create_all() db.create_all()
app.run(host='0.0.0.0', port=3000)
app.run(
host="0.0.0.0",
port=3000
)
+5 -3
View File
@@ -1,6 +1,8 @@
flask
flask_sqlalchemy
asyncio asyncio
aiohttp
apns2 apns2
configparser configparser
flask
flask_sqlalchemy
aiohttp
pyjwt
cryptography