253 lines
9.3 KiB
Python
253 lines
9.3 KiB
Python
# scripts/subscription_checker.py
|
|
|
|
import json
|
|
import datetime
|
|
import logging
|
|
import subprocess
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
from dateutil.parser import parse
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Path setup
|
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
PROJECT_ROOT = SCRIPT_DIR.parent
|
|
SUBSCRIPTION_DB = PROJECT_ROOT / 'data' / 'subscriptions.json'
|
|
CLEANUP_PLAYBOOK = PROJECT_ROOT / 'ansible' / 'playbooks' / 'vpn_cleanup.yml'
|
|
INVENTORY_FILE = PROJECT_ROOT / 'inventory.ini'
|
|
|
|
# Notification thresholds configuration
|
|
NOTIFICATION_THRESHOLDS = {
|
|
'minimum_duration': datetime.timedelta(hours=1), # Minimum subscription duration
|
|
'short_term': {
|
|
'max_duration': datetime.timedelta(days=1),
|
|
'warning_fraction': 0.5, # Warn when 50% of time remains
|
|
'grace_fraction': 0.1 # Grace period of 10% of subscription length
|
|
},
|
|
'medium_term': {
|
|
'max_duration': datetime.timedelta(days=7),
|
|
'warning_fraction': 0.25, # Warn when 25% of time remains
|
|
'grace_hours': 12 # Fixed 12-hour grace period
|
|
},
|
|
'long_term': {
|
|
'warning_days': 7, # 7 days warning for longer subscriptions
|
|
'grace_days': 2 # 2 days grace period
|
|
}
|
|
}
|
|
|
|
def load_subscriptions():
|
|
"""Load subscription data from JSON file"""
|
|
if not SUBSCRIPTION_DB.parent.exists():
|
|
SUBSCRIPTION_DB.parent.mkdir(parents=True)
|
|
|
|
if not SUBSCRIPTION_DB.exists():
|
|
return {}
|
|
|
|
with open(SUBSCRIPTION_DB, 'r') as f:
|
|
return json.load(f)
|
|
|
|
def save_subscriptions(subscriptions):
|
|
"""Save subscriptions to JSON file"""
|
|
with open(SUBSCRIPTION_DB, 'w') as f:
|
|
json.dump(subscriptions, f, indent=2)
|
|
|
|
def run_cleanup_playbook(sub_id):
|
|
"""Run the VPN cleanup playbook"""
|
|
logger.info(f"Running cleanup playbook for subscription {sub_id}")
|
|
|
|
vault_pass = os.getenv('ANSIBLE_VAULT_PASSWORD', '')
|
|
if not vault_pass:
|
|
raise Exception("Vault password not found in environment variables")
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', delete=False) as vault_pass_file:
|
|
vault_pass_file.write(vault_pass)
|
|
vault_pass_file.flush()
|
|
|
|
cmd = [
|
|
'ansible-playbook',
|
|
str(CLEANUP_PLAYBOOK),
|
|
'-i', str(INVENTORY_FILE),
|
|
'-e', f'subscription_id={sub_id}',
|
|
'--vault-password-file', vault_pass_file.name,
|
|
'-vvv'
|
|
]
|
|
|
|
logger.debug(f"Running command: {' '.join(cmd)}")
|
|
|
|
result = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True
|
|
)
|
|
|
|
if result.returncode != 0:
|
|
logger.error(f"Cleanup playbook failed: {result.stderr}")
|
|
else:
|
|
logger.debug(f"Cleanup playbook output: {result.stdout}")
|
|
|
|
os.unlink(vault_pass_file.name)
|
|
return result
|
|
|
|
def calculate_notification_times(start_time, end_time):
|
|
"""
|
|
Calculate warning and grace period times based on subscription duration
|
|
|
|
Returns:
|
|
tuple: (warning_time, grace_end_time)
|
|
"""
|
|
duration = end_time - start_time
|
|
|
|
# Handle extremely short subscriptions
|
|
if duration < NOTIFICATION_THRESHOLDS['minimum_duration']:
|
|
warning_time = start_time # Immediate warning
|
|
grace_end_time = end_time # No grace period
|
|
return warning_time, grace_end_time
|
|
|
|
# Short-term subscriptions (less than 1 day)
|
|
if duration <= NOTIFICATION_THRESHOLDS['short_term']['max_duration']:
|
|
warning_delta = duration * NOTIFICATION_THRESHOLDS['short_term']['warning_fraction']
|
|
grace_delta = duration * NOTIFICATION_THRESHOLDS['short_term']['grace_fraction']
|
|
warning_time = end_time - warning_delta
|
|
grace_end_time = end_time + grace_delta
|
|
|
|
# Medium-term subscriptions (1-7 days)
|
|
elif duration <= NOTIFICATION_THRESHOLDS['medium_term']['max_duration']:
|
|
warning_delta = duration * NOTIFICATION_THRESHOLDS['medium_term']['warning_fraction']
|
|
grace_hours = NOTIFICATION_THRESHOLDS['medium_term']['grace_hours']
|
|
warning_time = end_time - warning_delta
|
|
grace_end_time = end_time + datetime.timedelta(hours=grace_hours)
|
|
|
|
# Long-term subscriptions (> 7 days)
|
|
else:
|
|
warning_days = NOTIFICATION_THRESHOLDS['long_term']['warning_days']
|
|
grace_days = NOTIFICATION_THRESHOLDS['long_term']['grace_days']
|
|
warning_time = end_time - datetime.timedelta(days=warning_days)
|
|
grace_end_time = end_time + datetime.timedelta(days=grace_days)
|
|
|
|
return warning_time, grace_end_time
|
|
|
|
def get_notification_message(sub_data, remaining_time):
|
|
"""Generate appropriate notification message based on subscription duration"""
|
|
if remaining_time < datetime.timedelta(hours=1):
|
|
return f"Your VPN subscription expires in {int(remaining_time.total_seconds() / 60)} minutes!"
|
|
elif remaining_time < datetime.timedelta(days=1):
|
|
return f"Your VPN subscription expires in {int(remaining_time.total_seconds() / 3600)} hours!"
|
|
else:
|
|
return f"Your VPN subscription expires in {remaining_time.days} days!"
|
|
|
|
def notify_user(user_id, message):
|
|
"""Send notification to user about subscription status"""
|
|
try:
|
|
subscriptions = load_subscriptions()
|
|
sub_data = subscriptions.get(user_id)
|
|
|
|
if not sub_data or 'email' not in sub_data:
|
|
logger.error(f"No email found for user {user_id}")
|
|
return False
|
|
|
|
# Import BTCPayHandler here to avoid circular imports
|
|
from app.handlers.payment_handler import BTCPayHandler
|
|
|
|
btcpay_handler = BTCPayHandler()
|
|
email_sent = btcpay_handler.send_confirmation_email(
|
|
sub_data['email'],
|
|
f"""
|
|
VPN Subscription Update
|
|
|
|
{message}
|
|
|
|
If you wish to continue using the VPN service, please renew your subscription.
|
|
"""
|
|
)
|
|
|
|
if email_sent:
|
|
logger.info(f"Sent notification to {sub_data['email']}: {message}")
|
|
else:
|
|
logger.warning(f"Failed to send notification to {sub_data['email']}")
|
|
|
|
return email_sent
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error sending notification: {str(e)}")
|
|
return False
|
|
|
|
def check_subscriptions():
|
|
"""Check subscription status and clean up expired ones"""
|
|
logger.info("Starting subscription check")
|
|
|
|
subscriptions = load_subscriptions()
|
|
now = datetime.datetime.now()
|
|
modified = False
|
|
|
|
logger.info(f"Checking {len(subscriptions)} subscriptions")
|
|
|
|
for sub_id, sub_data in list(subscriptions.items()):
|
|
try:
|
|
logger.debug(f"Processing subscription {sub_id}")
|
|
|
|
if sub_data.get('status') != 'Active':
|
|
logger.debug(f"Skipping inactive subscription {sub_id}")
|
|
continue
|
|
|
|
start_time = parse(sub_data['start_time'])
|
|
expiry = parse(sub_data['expiry'])
|
|
warning_time, grace_end_time = calculate_notification_times(start_time, expiry)
|
|
|
|
# Calculate remaining time
|
|
remaining_time = expiry - now
|
|
logger.debug(f"Subscription {sub_id} has {remaining_time} remaining")
|
|
|
|
# Handle warnings
|
|
if now >= warning_time and not sub_data.get('warning_sent'):
|
|
message = get_notification_message(sub_data, remaining_time)
|
|
logger.info(f"Sending notification for subscription {sub_id}: {message}")
|
|
|
|
notify_user(sub_id, message)
|
|
|
|
sub_data['warning_sent'] = True
|
|
modified = True
|
|
|
|
# Handle expiration
|
|
if now >= grace_end_time and sub_data.get('status') == 'Active':
|
|
logger.info(f"Processing expiration for subscription {sub_id}")
|
|
|
|
try:
|
|
logger.debug(f"Running cleanup playbook for {sub_id}")
|
|
result = run_cleanup_playbook(sub_id)
|
|
|
|
if result.returncode == 0:
|
|
sub_data['status'] = 'Expired'
|
|
sub_data['cleanup_date'] = now.isoformat()
|
|
modified = True
|
|
logger.info(f"Successfully cleaned up subscription {sub_id}")
|
|
else:
|
|
logger.error(f"Cleanup failed: {result.stderr}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error during cleanup: {str(e)}")
|
|
logger.error(traceback.format_exc())
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error processing subscription {sub_id}: {str(e)}")
|
|
logger.error(traceback.format_exc())
|
|
continue
|
|
|
|
if modified:
|
|
save_subscriptions(subscriptions)
|
|
logger.info("Updated subscription database")
|
|
|
|
logger.info("Subscription check completed")
|
|
|
|
if __name__ == '__main__':
|
|
try:
|
|
check_subscriptions()
|
|
except Exception as e:
|
|
logger.error(f"Subscription checker failed: {str(e)}")
|
|
raise |