Compare commits
3 Commits
bdbdd5acfb
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| cb2c95b542 | |||
| a25a5efd9b | |||
| 181824b460 |
@@ -1,316 +1,178 @@
|
|||||||
import time
|
import time
|
||||||
import jwt
|
import jwt
|
||||||
|
import json
|
||||||
import httpx
|
import httpx
|
||||||
import asyncio
|
|
||||||
import configparser
|
import configparser
|
||||||
|
|
||||||
from flask import Flask, request, jsonify
|
from flask import Flask, request, jsonify, abort
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from sqlalchemy import create_engine, Column, Integer, String
|
||||||
|
from sqlalchemy.orm import declarative_base, sessionmaker
|
||||||
|
|
||||||
|
|
||||||
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["AUTH"]["REGISTER_AUTH"]
|
||||||
MANAGE_AUTH = config.get("AUTH", "MANAGE_AUTH", fallback=None)
|
MANAGE_AUTH = config["AUTH"]["MANAGE_AUTH"]
|
||||||
|
|
||||||
SQLALCHEMY_DATABASE_URI = config.get(
|
DATABASE_URI = config["DATABASE"]["SQLALCHEMY_DATABASE_URI"]
|
||||||
"DATABASE",
|
|
||||||
"SQLALCHEMY_DATABASE_URI",
|
|
||||||
fallback="sqlite:///device_tokens.db"
|
|
||||||
)
|
|
||||||
|
|
||||||
auth_key_path = config.get(
|
APNS_KEY_PATH = config["APNS"]["auth_key_path"]
|
||||||
"APNS",
|
APNS_KEY_ID = config["APNS"]["auth_key_id"]
|
||||||
"auth_key_path",
|
APNS_TEAM_ID = config["APNS"]["team_id"]
|
||||||
fallback="./APNSAuthKey.p8"
|
APNS_TOPIC = config["APNS"]["topic"]
|
||||||
)
|
|
||||||
|
|
||||||
auth_key_id = config.get("APNS", "auth_key_id", fallback=None)
|
APNS_URL = "https://api.push.apple.com/3/device/"
|
||||||
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([
|
Base = declarative_base()
|
||||||
REGISTER_AUTH,
|
engine = create_engine(DATABASE_URI, echo=False)
|
||||||
MANAGE_AUTH,
|
SessionLocal = sessionmaker(bind=engine)
|
||||||
auth_key_id,
|
|
||||||
team_id,
|
|
||||||
topic
|
class DeviceToken(Base):
|
||||||
]):
|
__tablename__ = "device_tokens"
|
||||||
raise ValueError("Fehlende Pflichtfelder in der Konfiguration!")
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
token = Column(String, unique=True, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
app.config["SQLALCHEMY_DATABASE_URI"] = SQLALCHEMY_DATABASE_URI
|
|
||||||
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
|
||||||
|
|
||||||
db = SQLAlchemy(app)
|
|
||||||
|
|
||||||
class DeviceToken(db.Model):
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
|
|
||||||
device_token = db.Column(
|
|
||||||
db.String(256),
|
|
||||||
nullable=False,
|
|
||||||
unique=True
|
|
||||||
)
|
|
||||||
|
|
||||||
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():
|
def get_auth_header():
|
||||||
return jwt.encode(
|
auth = request.headers.get("Authorization", "")
|
||||||
{
|
if not auth.startswith("Bearer "):
|
||||||
"iss": team_id,
|
return None
|
||||||
"iat": int(time.time())
|
return auth.replace("Bearer ", "", 1)
|
||||||
},
|
|
||||||
APNS_PRIVATE_KEY,
|
|
||||||
|
def require_register_auth():
|
||||||
|
if get_auth_header() != REGISTER_AUTH:
|
||||||
|
abort(401)
|
||||||
|
|
||||||
|
|
||||||
|
def require_manage_auth():
|
||||||
|
if get_auth_header() != MANAGE_AUTH:
|
||||||
|
abort(401)
|
||||||
|
|
||||||
|
|
||||||
|
def load_apns_key():
|
||||||
|
with open(APNS_KEY_PATH, "r") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
def create_apns_jwt():
|
||||||
|
headers = {
|
||||||
|
"alg": "ES256",
|
||||||
|
"kid": APNS_KEY_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"iss": APNS_TEAM_ID,
|
||||||
|
"iat": int(time.time())
|
||||||
|
}
|
||||||
|
|
||||||
|
token = jwt.encode(
|
||||||
|
payload,
|
||||||
|
load_apns_key(),
|
||||||
algorithm="ES256",
|
algorithm="ES256",
|
||||||
headers={
|
headers=headers
|
||||||
"alg": "ES256",
|
|
||||||
"kid": auth_key_id
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return token
|
||||||
|
|
||||||
async def send_notification(
|
|
||||||
device_token,
|
|
||||||
title,
|
|
||||||
body
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
|
|
||||||
jwt_token = generate_apns_token()
|
def send_apns_notification(device_token: str, payload: dict):
|
||||||
|
jwt_token = create_apns_jwt()
|
||||||
|
|
||||||
payload = {
|
headers = {
|
||||||
"aps": {
|
"authorization": f"bearer {jwt_token}",
|
||||||
"alert": {
|
"apns-topic": APNS_TOPIC,
|
||||||
"title": title,
|
"apns-push-type": "alert"
|
||||||
"body": body
|
}
|
||||||
},
|
|
||||||
"sound": "default",
|
|
||||||
"badge": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
headers = {
|
url = APNS_URL + device_token
|
||||||
"authorization": f"bearer {jwt_token}",
|
|
||||||
"apns-topic": topic,
|
|
||||||
"apns-push-type": "alert"
|
|
||||||
}
|
|
||||||
|
|
||||||
url = f"{APNS_URL}/3/device/{device_token}"
|
with httpx.Client(http2=True) as client:
|
||||||
|
response = client.post(url, headers=headers, json=payload)
|
||||||
|
|
||||||
async with httpx.AsyncClient(
|
return response.status_code, response.text
|
||||||
http2=True,
|
|
||||||
timeout=30.0
|
|
||||||
) as client:
|
|
||||||
|
|
||||||
response = await client.post(
|
|
||||||
url,
|
|
||||||
json=payload,
|
|
||||||
headers=headers
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"APNS Fehler "
|
|
||||||
f"{response.status_code} "
|
|
||||||
f"für {device_token}: "
|
|
||||||
f"{response.text}"
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"Push erfolgreich "
|
|
||||||
f"an {device_token}"
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"Fehler beim Senden "
|
|
||||||
f"an {device_token}: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@app.route("/api/registerDeviceToken", methods=["POST"])
|
@app.route("/api/registerDeviceToken", methods=["POST"])
|
||||||
def register_device_token():
|
def register_device():
|
||||||
|
require_register_auth()
|
||||||
|
|
||||||
if not authenticate(request, REGISTER_AUTH):
|
data = request.get_json()
|
||||||
return jsonify({
|
if not data or "device_token" not in data:
|
||||||
"error": "Unauthorized"
|
return jsonify({"error": "device_token required"}), 400
|
||||||
}), 401
|
|
||||||
|
|
||||||
try:
|
token = data["device_token"]
|
||||||
|
|
||||||
data = request.get_json()
|
session = SessionLocal()
|
||||||
|
exists = session.query(DeviceToken).filter_by(token=token).first()
|
||||||
|
|
||||||
if not data or "device_token" not in data:
|
if not exists:
|
||||||
return jsonify({
|
session.add(DeviceToken(token=token))
|
||||||
"error": "device_token fehlt"
|
session.commit()
|
||||||
}), 400
|
|
||||||
|
|
||||||
device_token = data["device_token"]
|
session.close()
|
||||||
|
|
||||||
existing = DeviceToken.query.filter_by(
|
return jsonify({"status": "registered"})
|
||||||
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 registriert"
|
|
||||||
}), 200
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
|
|
||||||
db.session.rollback()
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"error": "Interner Serverfehler",
|
|
||||||
"details": str(e)
|
|
||||||
}), 500
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/notify", methods=["POST"])
|
@app.route("/api/notify", methods=["POST"])
|
||||||
def notify():
|
def notify():
|
||||||
|
require_manage_auth()
|
||||||
|
|
||||||
if not authenticate(request, MANAGE_AUTH):
|
data = request.get_json()
|
||||||
return jsonify({
|
|
||||||
"error": "Unauthorized"
|
|
||||||
}), 401
|
|
||||||
|
|
||||||
try:
|
if not data:
|
||||||
|
return jsonify({"error": "invalid json"}), 400
|
||||||
|
|
||||||
data = request.get_json()
|
severity = data.get("severity")
|
||||||
|
message = data.get("notification")
|
||||||
|
|
||||||
severity = data.get("severity")
|
if severity not in ["info", "warning", "urgent", "danger"]:
|
||||||
notification_text = data.get("notification")
|
return jsonify({"error": "invalid severity"}), 400
|
||||||
|
|
||||||
if not severity or not notification_text:
|
if not message:
|
||||||
return jsonify({
|
return jsonify({"error": "notification required"}), 400
|
||||||
"error": (
|
|
||||||
"severity und notification "
|
|
||||||
"sind erforderlich"
|
|
||||||
)
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
severity_icons = {
|
payload = {
|
||||||
"info": "",
|
"aps": {
|
||||||
"warning": "⚠️ ",
|
"alert": message,
|
||||||
"urgent": "❗️ ",
|
"sound": "default"
|
||||||
"danger": "❌ "
|
},
|
||||||
}
|
"severity": severity
|
||||||
|
}
|
||||||
|
|
||||||
if severity not in severity_icons:
|
session = SessionLocal()
|
||||||
return jsonify({
|
tokens = session.query(DeviceToken).all()
|
||||||
"error": "Ungültige severity"
|
session.close()
|
||||||
}), 400
|
|
||||||
|
|
||||||
title = (
|
results = []
|
||||||
f"{severity_icons[severity]}"
|
|
||||||
f"{notification_text}"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def send_all():
|
for t in tokens:
|
||||||
|
status, resp = send_apns_notification(t.token, payload)
|
||||||
tasks = []
|
results.append({
|
||||||
|
"token": t.token,
|
||||||
for token in DeviceToken.query.all():
|
"status": status,
|
||||||
|
"response": resp
|
||||||
tasks.append(
|
})
|
||||||
send_notification(
|
|
||||||
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:
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"error": "Interner Serverfehler",
|
|
||||||
"details": str(e)
|
|
||||||
}), 500
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/showdevicetokens", methods=["GET"])
|
|
||||||
def show_device_tokens():
|
|
||||||
|
|
||||||
tokens = DeviceToken.query.all()
|
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"device_tokens": [
|
"sent": len(results),
|
||||||
token.device_token
|
"results": results
|
||||||
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__":
|
||||||
|
app.run(host="0.0.0.0", port=3000, debug=True)
|
||||||
with app.app_context():
|
|
||||||
db.create_all()
|
|
||||||
|
|
||||||
app.run(
|
|
||||||
host="0.0.0.0",
|
|
||||||
port=3000
|
|
||||||
)
|
|
||||||
@@ -11,5 +11,3 @@ auth_key_id=dein_auth_key_id
|
|||||||
team_id=dein_team_id
|
team_id=dein_team_id
|
||||||
topic=dein_topic
|
topic=dein_topic
|
||||||
|
|
||||||
[API]
|
|
||||||
resultsapi=https://foobar.regattatech.de/api/v1/results
|
|
||||||
|
|||||||
+3
-7
@@ -1,9 +1,5 @@
|
|||||||
asyncio
|
|
||||||
apns2
|
|
||||||
configparser
|
|
||||||
flask
|
flask
|
||||||
flask_sqlalchemy
|
sqlalchemy
|
||||||
aiohttp
|
httpx[http2]
|
||||||
pyjwt
|
pyjwt
|
||||||
cryptography
|
cryptography
|
||||||
httpx
|
|
||||||
Reference in New Issue
Block a user