import os import re from datetime import datetime import click from flask.cli import with_appcontext from flask import Blueprint from flask import current_app from psycopg2 import ProgrammingError from flask_mail import Message from capsulflask.db import get_model, my_exec_info_message from capsulflask.console import get_account_balance bp = Blueprint('cli', __name__) @bp.cli.command('sql') @click.option('-f', help='script filename') @click.option('-c', help='sql command') @with_appcontext def sql_script(f, c): """Run a sql script against the database. script is run 1 command at a time inside a single transaction.""" model = get_model() script = "" if f: filepath = os.path.join(os.getcwd(), f) if not os.path.isfile(filepath): raise f"{filepath} is not a file" with open(filepath, 'rb') as file: script = file.read().decode("utf8") elif c: script = c else: click.echo(f"you must provide sql to run either inline with the -c argument or in a file with the -f argument") return commands = re.split(";\\s+", script) for command in commands: if command.strip() != "": model.cursor.execute(command) if re.match("^\\s*select", command, re.IGNORECASE) is not None: for row in model.cursor.fetchall(): def format_value(x): if isinstance(x, bool): return "true" if x else "false" if not x : return "null" if isinstance(x, datetime): return x.isoformat() return f"{x}" click.echo(", ".join(list(map(format_value, row)))) else: click.echo(f"{model.cursor.rowcount} rows affected.") model.connection.commit() @bp.cli.command('cron-task') @with_appcontext def cron_task(): # make sure btcpay payments get completed (in case we miss a webhook), otherwise invalidate the payment unresolved_btcpay_invoices = get_model().get_unresolved_btcpay_invoices() for invoice in unresolved_btcpay_invoices: invoice_id = invoice.id invoice = current_app.config['BTCPAY_CLIENT'].get_invoice(invoice_id) days = float((datetime.now() - invoice.created).total_seconds())/float(60*60*24) if invoice['status'] == "complete": get_model().btcpay_invoice_resolved(invoice_id, True) elif days >= 1: get_model().btcpay_invoice_resolved(invoice_id, False) # notify when funds run out accounts = get_model().accounts_list() for account in accounts: vms = get_model().list_vms_for_account(account['email']) payments = get_model().list_payments_for_account(account['email']) balance_1w = get_account_balance(vms, payments, datetime.utcnow() + datetime.timedelta(days=7)) balance_1d = get_account_balance(vms, payments, datetime.utcnow() + datetime.timedelta(days=1)) balance_now = get_account_balance(vms, payments, datetime.utcnow()) current_warning = account['account_balance_warning'] delete_at_account_balance = -10 pluralize_capsul = "s" if len(vms) > 1 else "" warnings = [ dict( id='zero_1w', active=balance_1w < 0, subject="Capsul One Week Payment Reminder", body=("According to our calculations, your Capsul account will run out of funds in one week.\n\n" f"Log in now to re-fill your account! {current_app.config['BASE_URL']}/console/account-balance\n\n" "If you believe you have recieved this message in error, please let us know: support@cyberia.club") ), dict( id='zero_1d', active=balance_1d < 0, subject="Capsul One Day Payment Reminder", body=("According to our calculations, your Capsul account will run out of funds tomorrow.\n\n" f"Log in now to re-fill your account! {current_app.config['BASE_URL']}/console/account-balance\n\n" "If you believe you have recieved this message in error, please let us know: support@cyberia.club") ), dict( id='zero_now', active=balance_now < 0, subject="Your Capsul Account is No Longer Funded", body=(f"You have run out of funds! You will no longer be able to create Capsul{pluralize_capsul}.\n\n" f"As a courtesy, we'll let your existing Capsul{pluralize_capsul} keep running until your account " "reaches a -$10 balance, at which point they will be deleted.\n\n" f"Log in now to re-fill your account! {current_app.config['BASE_URL']}/console/account-balance\n\n" f"If you need help decomissioning your Capsul{pluralize_capsul}, " "would like to request backups, or de-activate your account, please contact: support@cyberia.club") ), dict( id='delete_1w', active=balance_1w < delete_at_account_balance, subject=f"Your Capsul{pluralize_capsul} Will be Deleted In Less Than a Week", body=("You have run out of funds and have not refilled your account.\n\n" f"As a courtesy, we have let your existing Capsul{pluralize_capsul} keep running. " f"However, your account will reach a -$10 balance some time next week and your Capsul{pluralize_capsul}" "will be deleted.\n\n" f"Log in now to re-fill your account! {current_app.config['BASE_URL']}/console/account-balance\n\n" f"If you need help decomissioning your Capsul{pluralize_capsul}, " "would like to request backups, or de-activate your account, please contact: support@cyberia.club") ), dict( id='delete_1d', active=balance_1d < delete_at_account_balance, subject=f"Last Chance to Save your Capsul{pluralize_capsul}: Gone Tomorrow", body=("You have run out of funds and have not refilled your account.\n\n" f"As a courtesy, we have let your existing Capsul{pluralize_capsul} keep running. " f"However, your account will reach a -$10 balance tomorrow and your Capsul{pluralize_capsul} will be deleted.\n\n" f"Last chance to deposit funds now and keep your Capsul{pluralize_capsul} running! {current_app.config['BASE_URL']}/console/account-balance") ), dict( id='delete_now', active=balance_now < delete_at_account_balance, subject=f"Capsul{pluralize_capsul} Deleted", body=(f"Your account reached a -$10 balance and your Capsul{pluralize_capsul} were deleted.") ) ] current_warning_index = -1 if current_warning: for i in range(0, len(warnings)): if warnings[i]['id'] == current_warning: current_warning_index = i index_to_send = -1 for i in range(0, len(warnings)): if i > current_warning_index and warnings[i].active: index_to_send = i if index_to_send > -1: print(f"cron_task: sending {warnings[index_to_send]['id']} warning email to {account['email']}.") current_app.config["FLASK_MAIL_INSTANCE"].send( Message( warnings[index_to_send]['subject'], body=warnings[index_to_send]['body'], recipients=[account['email']] ) ) get_model().set_account_balance_warning(account['email'], warnings[index_to_send]['id']) if index_to_send == len(warnings)-1: for vm in vms: print(f"cron_task: deleting {vm['id']} ( {account['email']} ) due to negative account balance.") current_app.config["VIRTUALIZATION_MODEL"].destroy(email=account["email"], id=vm['id']) get_model().delete_vm(email=account["email"], id=vm['id']) # make sure vm system and DB are synced