2025-07-17 15:41:47 +01:00
|
|
|
from flask import Blueprint, jsonify, Response, request, current_app
|
2025-07-15 15:45:17 +01:00
|
|
|
from ktvmanager.lib.database import (
|
|
|
|
get_user_accounts,
|
|
|
|
get_stream_names,
|
|
|
|
single_check,
|
|
|
|
add_account,
|
|
|
|
delete_account,
|
|
|
|
get_user_id_from_username,
|
2025-07-17 15:41:47 +01:00
|
|
|
save_push_subscription,
|
|
|
|
get_push_subscriptions,
|
2025-07-15 15:45:17 +01:00
|
|
|
)
|
2025-07-13 19:40:04 +01:00
|
|
|
from ktvmanager.lib.get_urls import get_latest_urls_from_dns
|
2025-07-15 09:28:47 +01:00
|
|
|
from ktvmanager.lib.auth import requires_basic_auth, check_login
|
2025-07-15 15:42:47 +01:00
|
|
|
from ktvmanager.lib.checker import validate_account
|
2025-07-15 15:45:17 +01:00
|
|
|
from typing import Tuple
|
2025-07-17 15:41:47 +01:00
|
|
|
import json
|
2025-07-17 17:19:31 +01:00
|
|
|
import re
|
2025-07-17 15:41:47 +01:00
|
|
|
from pywebpush import webpush, WebPushException
|
2025-07-13 19:40:04 +01:00
|
|
|
|
|
|
|
api_blueprint = Blueprint("api", __name__)
|
|
|
|
|
2025-07-15 15:45:17 +01:00
|
|
|
|
2025-07-13 19:40:04 +01:00
|
|
|
@api_blueprint.route("/getUserAccounts")
|
|
|
|
@requires_basic_auth
|
2025-07-15 15:45:17 +01:00
|
|
|
def get_user_accounts_route(username: str, password: str) -> Response:
|
|
|
|
"""Retrieves all accounts associated with a user.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
username: The username of the user.
|
|
|
|
password: The password of the user (used for authentication).
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
A Flask JSON response containing the user's accounts or an error message.
|
|
|
|
"""
|
2025-07-13 19:40:04 +01:00
|
|
|
user_id = get_user_id_from_username(username)
|
|
|
|
if user_id:
|
|
|
|
return get_user_accounts(user_id)
|
|
|
|
return jsonify({"message": "User not found"}), 404
|
|
|
|
|
2025-07-15 15:45:17 +01:00
|
|
|
|
2025-07-13 19:40:04 +01:00
|
|
|
@api_blueprint.route("/getStreamNames")
|
|
|
|
@requires_basic_auth
|
2025-07-15 15:45:17 +01:00
|
|
|
def get_stream_names_route(username: str, password: str) -> Response:
|
|
|
|
"""Retrieves all stream names.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
username: The username of the user.
|
|
|
|
password: The password of the user (used for authentication).
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
A Flask JSON response containing the list of stream names.
|
|
|
|
"""
|
2025-07-13 19:40:04 +01:00
|
|
|
return get_stream_names()
|
|
|
|
|
2025-07-15 15:45:17 +01:00
|
|
|
|
2025-07-13 19:40:04 +01:00
|
|
|
@api_blueprint.route("/getUserAccounts/streams")
|
|
|
|
@requires_basic_auth
|
2025-07-15 15:45:17 +01:00
|
|
|
def get_user_accounts_streams_route(username: str, password: str) -> Response:
|
|
|
|
"""Retrieves the latest stream URLs from DNS.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
username: The username of the user.
|
|
|
|
password: The password of the user (used for authentication).
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
A Flask JSON response containing the list of stream URLs.
|
|
|
|
"""
|
2025-07-13 19:40:04 +01:00
|
|
|
return jsonify(get_latest_urls_from_dns())
|
|
|
|
|
2025-07-15 15:45:17 +01:00
|
|
|
|
2025-07-13 19:40:04 +01:00
|
|
|
@api_blueprint.route("/singleCheck", methods=["POST"])
|
|
|
|
@requires_basic_auth
|
2025-07-15 15:45:17 +01:00
|
|
|
def single_check_route(username: str, password: str) -> Response:
|
|
|
|
"""Performs a single account check.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
username: The username of the user.
|
|
|
|
password: The password of the user (used for authentication).
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
A Flask JSON response with the result of the check.
|
|
|
|
"""
|
2025-07-13 19:40:04 +01:00
|
|
|
return single_check()
|
|
|
|
|
2025-07-15 15:45:17 +01:00
|
|
|
|
2025-07-13 19:40:04 +01:00
|
|
|
@api_blueprint.route("/addAccount", methods=["POST"])
|
|
|
|
@requires_basic_auth
|
2025-07-15 15:45:17 +01:00
|
|
|
def add_account_route(username: str, password: str) -> Response:
|
|
|
|
"""Adds a new account for the user.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
username: The username of the user.
|
|
|
|
password: The password of the user (used for authentication).
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
A Flask JSON response confirming the account was added or an error message.
|
|
|
|
"""
|
2025-07-14 19:18:47 +01:00
|
|
|
user_id = get_user_id_from_username(username)
|
|
|
|
return add_account(user_id)
|
2025-07-13 19:40:04 +01:00
|
|
|
|
2025-07-15 15:45:17 +01:00
|
|
|
|
2025-07-13 19:40:04 +01:00
|
|
|
@api_blueprint.route("/deleteAccount", methods=["POST"])
|
|
|
|
@requires_basic_auth
|
2025-07-15 15:45:17 +01:00
|
|
|
def delete_account_route(username: str, password: str) -> Response:
|
|
|
|
"""Deletes an account for the user.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
username: The username of the user.
|
|
|
|
password: The password of the user (used for authentication).
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
A Flask JSON response confirming the account was deleted or an error message.
|
|
|
|
"""
|
2025-07-14 19:18:47 +01:00
|
|
|
user_id = get_user_id_from_username(username)
|
2025-07-15 09:28:47 +01:00
|
|
|
return delete_account(user_id)
|
|
|
|
|
2025-07-15 15:45:17 +01:00
|
|
|
|
2025-07-15 15:12:44 +01:00
|
|
|
@api_blueprint.route("/validateAccount", methods=["POST"])
|
|
|
|
@requires_basic_auth
|
2025-07-15 15:45:17 +01:00
|
|
|
def validate_account_route(username: str, password: str) -> Tuple[Response, int]:
|
|
|
|
"""Validates an account.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
username: The username of the user.
|
|
|
|
password: The password of the user (used for authentication).
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
A tuple containing a Flask JSON response and an HTTP status code.
|
|
|
|
"""
|
2025-07-15 15:12:44 +01:00
|
|
|
return validate_account()
|
|
|
|
|
2025-07-15 15:45:17 +01:00
|
|
|
|
2025-07-15 09:28:47 +01:00
|
|
|
@api_blueprint.route("/Login")
|
|
|
|
@requires_basic_auth
|
2025-07-15 15:45:17 +01:00
|
|
|
def login_route(username: str, password: str) -> Response:
|
|
|
|
"""Logs a user in.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
username: The username of the user.
|
|
|
|
password: The password of the user.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
A Flask JSON response with the result of the login attempt.
|
|
|
|
"""
|
2025-07-17 15:41:47 +01:00
|
|
|
return check_login(username, password)
|
|
|
|
|
|
|
|
|
2025-07-17 16:23:16 +01:00
|
|
|
@api_blueprint.route("/vapid-public-key", methods=["GET"])
|
|
|
|
def vapid_public_key():
|
2025-07-17 17:19:31 +01:00
|
|
|
"""Provides the VAPID public key in the correct format."""
|
|
|
|
pem_key = current_app.config["VAPID_PUBLIC_KEY"]
|
|
|
|
# Use regex to robustly extract the base64 content from the PEM key
|
|
|
|
match = re.search(r"-----BEGIN PUBLIC KEY-----(.*)-----END PUBLIC KEY-----", pem_key, re.DOTALL)
|
|
|
|
if not match:
|
|
|
|
return jsonify({"error": "Could not parse VAPID public key from config"}), 500
|
|
|
|
|
|
|
|
# Join the split lines to remove all whitespace and newlines
|
|
|
|
base64_key = "".join(match.group(1).split())
|
2025-07-17 17:30:02 +01:00
|
|
|
|
2025-07-17 17:49:50 +01:00
|
|
|
# Convert to URL-safe base64 and remove padding for the PushManager API
|
|
|
|
url_safe_key = base64_key.replace('+', '-').replace('/', '_').rstrip('=')
|
2025-07-17 17:30:02 +01:00
|
|
|
|
|
|
|
return jsonify({"public_key": url_safe_key})
|
2025-07-17 16:23:16 +01:00
|
|
|
|
|
|
|
|
2025-07-17 15:41:47 +01:00
|
|
|
@api_blueprint.route("/save-subscription", methods=["POST"])
|
|
|
|
@requires_basic_auth
|
|
|
|
def save_subscription(username: str, password: str) -> Response:
|
|
|
|
"""Saves a push notification subscription.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
username: The username of the user.
|
|
|
|
password: The password of the user.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
A Flask JSON response.
|
|
|
|
"""
|
|
|
|
user_id = get_user_id_from_username(username)
|
|
|
|
if not user_id:
|
|
|
|
return jsonify({"message": "User not found"}), 404
|
|
|
|
|
|
|
|
subscription_data = request.get_json()
|
|
|
|
if not subscription_data:
|
|
|
|
return jsonify({"message": "No subscription data provided"}), 400
|
|
|
|
|
|
|
|
save_push_subscription(user_id, json.dumps(subscription_data))
|
|
|
|
return jsonify({"message": "Subscription saved."})
|
|
|
|
|
|
|
|
|
|
|
|
def send_notification(subscription_info, message_body):
|
|
|
|
try:
|
|
|
|
webpush(
|
|
|
|
subscription_info=subscription_info,
|
|
|
|
data=message_body,
|
|
|
|
vapid_private_key=current_app.config["VAPID_PRIVATE_KEY"],
|
2025-07-17 18:13:07 +01:00
|
|
|
vapid_claims={"sub": current_app.config["VAPID_CLAIM_EMAIL"]},
|
2025-07-17 15:41:47 +01:00
|
|
|
)
|
|
|
|
except WebPushException as ex:
|
|
|
|
print(f"Web push error: {ex}")
|
|
|
|
# You might want to remove the subscription if it's invalid
|
|
|
|
if ex.response and ex.response.status_code == 410:
|
|
|
|
print("Subscription is no longer valid, removing from DB.")
|
|
|
|
# Add logic to remove the subscription from your database
|
|
|
|
|
|
|
|
|
|
|
|
@api_blueprint.route("/send-expiry-notifications", methods=["POST"])
|
|
|
|
@requires_basic_auth
|
|
|
|
def send_expiry_notifications_route(username: str, password: str) -> Response:
|
|
|
|
"""Triggers the sending of expiry notifications.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
username: The username of the user.
|
|
|
|
password: The password of the user.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
A Flask JSON response.
|
|
|
|
"""
|
|
|
|
from ktvmanager.account_checker import send_expiry_notifications
|
|
|
|
send_expiry_notifications()
|
2025-07-17 19:06:03 +01:00
|
|
|
return jsonify({"message": "Expiry notifications sent."})
|
|
|
|
|
|
|
|
|
|
|
|
@api_blueprint.route("/send-test-notification", methods=["POST"])
|
|
|
|
@requires_basic_auth
|
|
|
|
def send_test_notification_route(username: str, password: str) -> Response:
|
|
|
|
"""Sends a test push notification to the user."""
|
|
|
|
user_id = get_user_id_from_username(username)
|
|
|
|
if not user_id:
|
|
|
|
return jsonify({"message": "User not found"}), 404
|
|
|
|
|
|
|
|
subscriptions = get_push_subscriptions(user_id)
|
|
|
|
if not subscriptions:
|
|
|
|
return jsonify({"message": "No push subscriptions found for this user."}), 404
|
|
|
|
|
|
|
|
message_body = json.dumps({"title": "Test Notification", "body": "This is a test notification."})
|
|
|
|
|
|
|
|
for sub in subscriptions:
|
|
|
|
try:
|
|
|
|
send_notification(json.loads(sub['subscription_json']), message_body)
|
|
|
|
except Exception as e:
|
|
|
|
print(f"Error sending notification to subscription ID {sub.get('id', 'N/A')}: {e}")
|
|
|
|
|
|
|
|
return jsonify({"message": f"Test notification sent to {len(subscriptions)} subscription(s)."})
|