import sqlite3 from flask import g from flask import Flask app = Flask(__name__) from functools import wraps from flask import request, Response from flask import render_template, redirect from funkapi import FunkAPI import json import time import datetime from dateutil.relativedelta import relativedelta from apscheduler.schedulers.background import BackgroundScheduler import os import atexit DATABASE = os.getenv("FUNK_DATABASE", 'database.db') USERNAME = os.getenv("FUNK_USER", 'admin') PASSWORD = os.getenv("FUNK_PASS", 'geheim') ENABLED_AUTH = os.getenv("FUNK_AUTH", False) UPDATE_INTERVAL = int(os.getenv("FUNK_UPDATE_INTERVAL", 60*10)) plans = [] plans.append({"name": "Unlimit", "number": "8", "canBeBooked": True}) plans.append({"name": "1 GB", "number": "9", "canBeBooked": True}) plans.append({"name": "Break", "number": "42", "canBeBooked": True}) plans.append({"name": "Unlimit - First Day", "number": "40", "canBeBooked": False}) plans.append({"name": "Unlimit - Immidiately", "number": "43", "canBeBooked": False}) #Helper def get_db(): db = getattr(g, '_database', None) if db is None: db = g._database = sqlite3.connect(DATABASE) return db def check_auth(username, password): """This function is called to check if a username / password combination is valid. """ return username == USERNAME and password == PASSWORD def authenticate(): """Sends a 401 response that enables basic auth""" return Response( 'Could not verify your access level for that URL.\n' 'You have to login with proper credentials', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'}) def requires_auth(f): @wraps(f) def decorated(*args, **kwargs): if not ENABLED_AUTH: return f(*args, **kwargs) auth = request.authorization if not auth or not check_auth(auth.username, auth.password): return authenticate() return f(*args, **kwargs) return decorated # Functions def function_cron(): print("Run Cron") with app.app_context(): print("Debug Mode:" + str(int(app.debug))) if app.debug == False: function_updateAccountsAtFunk() else: print("Cron don't run in debug mode") def function_updateAllAccounts(): accounts = function_getAccounts() for account in accounts: function_updateAccount(account["number"]) def function_updateAccount(number, api = None): cur = get_db().cursor() if api == None: cur.execute("SELECT mail, password FROM accounts WHERE `number` = '%s'" % (number)) res = cur.fetchone() api = FunkAPI(res[0], res[1]) usage = api.getData()["data"]["me"]["customerProducts"][0]["mobileNumbers"][0]["usage"]["usedDataPercentage"] plan = getCurrentPlan(api) if plan == None: plan = 42; else: plan = plan["productServiceId"] cur.execute("INSERT INTO updates VALUES (%s, %s, '%s', '%s')" % ( number, int(time.time()), plan, usage )) get_db().commit() def function_speacialDays(number): cur = get_db().cursor() cur.execute("SELECT day, month, year, plan FROM special WHERE number = %s ORDER BY year, month, day" % number) res = cur.fetchall() days = [] for r in res: day = {} day["day"] = str(r[0])+"."+str(r[1])+"."+str(r[2]) day["plan"] = function_getPlanName(r[3]) days.append(day) return days def getCurrentPlan(api, now = datetime.datetime.now(datetime.timezone.utc)): currentPlan = None for plan in api.getData(refresh=False)["data"]["me"]["customerProducts"][0]["tariffs"]: planStart = datetime.datetime.strptime(plan["starts"], "%Y-%m-%dT%H:%M:%S.%f%z") if planStart > now: continue currentPlan = plan return currentPlan def function_getPlanName(planNumber): for plan in plans: if plan["number"] == planNumber: return plan return False def function_getAccounts(includePassword = False): cur = get_db().cursor() cur.execute("SELECT number, mail, password, owner, defaultPlan FROM accounts") res = cur.fetchall() accounts = [] for accountData in res: lastUpdate = cur.execute("SELECT updateTime, plan, dataUsed FROM updates WHERE number = '%s' ORDER BY updateTime DESC LIMIT 1" % (accountData[0])).fetchone() account = {} account["number"] = accountData[0] account["mail"] = accountData[1] if includePassword: account["password"] = accountData[2] account["owner"] = accountData[3] account["defaultPlan"] = function_getPlanName(accountData[4]) account["currentPlan"] = function_getPlanName(lastUpdate[1]) account["dataUsed"] = lastUpdate[2] account["lastUpdate"] = lastUpdate[0] accounts.append(account) return accounts def function_getAccount(number, includePassword = False): cur = get_db().cursor() cur.execute("SELECT number, mail, password, owner, defaultPlan FROM accounts WHERE number = %s" % (number)) accountData = cur.fetchone() lastUpdate = cur.execute("SELECT updateTime, plan, dataUsed FROM updates WHERE number = '%s' ORDER BY updateTime DESC LIMIT 1" % (accountData[0])).fetchone() account = {} account["number"] = accountData[0] account["mail"] = accountData[1] if includePassword: account["password"] = accountData[2] account["owner"] = accountData[3] account["defaultPlan"] = function_getPlanName(accountData[4]) account["currentPlan"] = function_getPlanName(lastUpdate[1]) account["dataUsed"] = lastUpdate[2] account["lastUpdate"] = lastUpdate[0] return account def function_getPlanForDay(number, year, month, day): cur = get_db().cursor() dayPlan = cur.execute("SELECT plan FROM special WHERE number = %s AND day = %s AND month = %s AND year = %s" % (number, day, month, year)).fetchone() if dayPlan: return dayPlan[0] else: defaultPlan = cur.execute("SELECT defaultPlan FROM accounts WHERE number = %s" % (number)).fetchone() return defaultPlan[0] def function_updateAccountsAtFunk(): print("Send Updates to Funk") accounts = function_getAccounts(True) tomorrow = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=1) today = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=1) for account in accounts: api = FunkAPI(account["mail"], account["password"]) function_updateAccount(account["number"], api) # We already have the API with the Data, so we can write them in the update table planedPlan = function_getPlanForDay(account["number"], tomorrow.year, tomorrow.month, tomorrow.day) plan = getCurrentPlan(api, tomorrow) currentPlan = getCurrentPlan(api) if currentPlan == 42: # Wenn aktuell eine pause ist darf das update erst am selben tag erfolgen wie der neue Plan gültug ist planedPlan = function_getPlanForDay(account["number"], today.year, today.month, today.day) if planedPlan == "8": #Change to unlimit api.orderUnlimitedPlan() print("Switch to Unlimit") elif planedPlan == "9": #Change to 1GB api.order1GBPlan() print("Switch to 1GB") elif planedPlan == "42": #Change to break api.startPause() print("Switch to break") else: print("Cant update plan!!! ERROR") else: if(plan["productServiceId"] == planedPlan): print("Nothing to Change, plan already set") else: print("Update") if planedPlan == "8": #Change to unlimit api.orderUnlimitedPlan() print("Switch to Unlimit") elif planedPlan == "9": #Change to 1GB api.order1GBPlan() print("Switch to 1GB") elif planedPlan == "42": #Change to break api.startPause() print("Switch to break") else: print("Cant update plan!!! ERROR") #Routes @app.route("/") @requires_auth def hello(): resData = function_getAccounts() plans = {} plans["1 GB"] = 0; plans["Unlimit"] = 0; plans["Unlimit - First Day"] = 0; plans["Break"] = 0 for account in resData: plans[account["currentPlan"]["name"]] = plans[account["currentPlan"]["name"]] + 1; return render_template("dashboard.html", debug=app.debug, plans=plans) @app.route('/setup', methods=['GET']) @requires_auth def setupDB(): cur = get_db().cursor() cur.execute('''CREATE TABLE accounts (number integer, mail text, password text, owner text, defaultPlan text)''') cur.execute('''CREATE TABLE updates (number integer, updateTime integer, plan text, dataUsed integer)''') cur.execute('''CREATE TABLE special (number integer, day integer, month integer, year integer, plan text)''') cur.execute('''CREATE TABLE run_actions (year integer, month integer, day integer)''') get_db().commit() return "Accounts" # GUI @app.route('/sendToFunk', methods=['GET']) @requires_auth def sendToFunkByGui(): function_updateAccountsAtFunk() return "Update send to funk" @app.route("/accounts") @requires_auth def gui_liste(): return render_template("list.html", debug=app.debug, accounts=function_getAccounts(), currentTime=int(time.time())) @app.route("/account/") @requires_auth def gui_account(number): account = function_getAccount(number) special_days = function_speacialDays(number) return render_template("account.html", debug=app.debug, account=account, plans=plans, special_days=special_days, currentTime=int(time.time())) @app.route("/account//statistics") @requires_auth def gui_statistic(number): cur = get_db().cursor() ende = datetime.datetime.now() start = ende - relativedelta(months=1) delta = ende - start dayList = [] for i in range(delta.days + 1): day = {} date = start + datetime.timedelta(days=i) dayStart = datetime.datetime(date.year, date.month, date.day, 0, 0, 0) dayEnde = datetime.datetime(date.year, date.month, date.day, 23, 59, 59) day["date"] = str(date.day)+"."+str(date.month)+"."+str(date.year) lastUpdate = cur.execute("SELECT dataUsed, updateTime FROM `updates` WHERE `number` = %s AND `updateTime` > %s AND `updateTime` < %s ORDER BY `updateTime` DESC" % (number, int(dayStart.timestamp()), int(dayEnde.timestamp()))).fetchone() if lastUpdate == None: day["usage"] = 0 else: day["usage"] = round(lastUpdate[0], 4); dayList.append(day) return render_template("statistics.html", debug=app.debug, dayList=dayList) @app.route("/account//special", methods=['POST']) @requires_auth def special_days(number): start = datetime.datetime(int(request.form["from_year"]), int(request.form["from_month"]), int(request.form["from_day"]), 0, 0, 0) ende = datetime.datetime(int(request.form["to_year"]), int(request.form["to_month"]), int(request.form["to_day"]), 0, 0, 0) delta = ende - start cur = get_db().cursor() for i in range(delta.days + 1): date = start + datetime.timedelta(days=i) if request.form["plan"] == "Default": cur.execute("DELETE FROM special WHERE number = %s AND year = %s AND month = %s AND day = %s" % (number, date.year, date.month, date.day)) else: cur.execute("DELETE FROM special WHERE number = %s AND year = %s AND month = %s AND day = %s" % (number, date.year, date.month, date.day)) cur.execute("INSERT INTO special VALUES (%s, %s, %s, %s, '%s')" % (number, date.day, date.month, date.year, request.form["plan"])) get_db().commit(); return redirect("/account/"+str(number), code=302) @app.route("/account//days") @requires_auth def list_days(number): start = datetime.datetime.now() ende = start + relativedelta(months=2) delta = ende - start dayList = [] for i in range(delta.days + 1): day = {} date = start + datetime.timedelta(days=i) day["date"] = str(date.day)+"."+str(date.month)+"."+str(date.year) day["plan"] = function_getPlanName(function_getPlanForDay(number, date.year, date.month, date.day)) dayList.append(day) return render_template("list_days.html", debug=app.debug, dayList=dayList) @app.route("/account/add", methods=['GET']) @requires_auth def add_account_gui(): return render_template("account_add.html") @app.route("/account/add", methods=['POST']) @requires_auth def add_account_gui_save(): r = addAccount() return redirect("/accounts", code=302) # API @app.route('/api/accounts', methods=['GET']) @requires_auth def listAccounts(): resData = function_getAccounts() response = {"success": True, "data": resData} return json.dumps(response) @app.route('/api/account//update', methods=['GET']) @requires_auth def updateAccount(number): function_updateAccount(number) response = {"success": True} return json.dumps(response) @app.route('/api/account//changeDafault', methods=['POST']) @requires_auth def changeDefault(number): cur = get_db().cursor() default = request.form["plan"] cur.execute("UPDATE accounts SET `defaultPlan` = '%s' WHERE `number` = %s" % (default, number)) get_db().commit(); response = {"success": True} return json.dumps(response) @app.route('/api/accounts', methods=['POST']) @requires_auth def addAccount(): cur = get_db().cursor() mail = request.form["mail"] password = request.form["password"] response = {} response["success"] = False response["msg"] = "" try: api = FunkAPI(mail, password) personalData = api.getPersonalInfo() except Exception: response["msg"] = "Autherized failed, wrong mail/password?" return json.dumps(response) plan = getCurrentPlan(api)["productServiceId"] name = personalData["firstName"]+" "+personalData["lastName"] defaultPlan = 42 for plan in plans: if plan["number"] == plan: defaultPlan = plan # getUsage data = api.getData() number = data["data"]["me"]["customerProducts"][0]["mobileNumbers"][0]["number"] #number integer, mail text, password text, owner text, defaultPlan text cur.execute("INSERT INTO accounts VALUES ('%s', '%s', '%s', '%s', '%s')" % ( number, mail, password, name, defaultPlan )) get_db().commit() function_updateAccount(number) response["success"] = True response["data"] = {"name": name} return json.dumps(response) #SQLITE @app.teardown_appcontext def close_connection(exception): db = getattr(g, '_database', None) if db is not None: db.close() atexit.register(lambda: scheduler.shutdown()) def print_date_time(): print(time.strftime("%A, %d. %B %Y %I:%M:%S %p")) if app.debug == False: print("Start cron"); print("Cron interval: "+str(UPDATE_INTERVAL)) scheduler = BackgroundScheduler() scheduler.add_job(func=function_cron, trigger="interval", seconds=(UPDATE_INTERVAL)) scheduler.start() else: print("Dont start crons in Debug Mode!") if __name__ == '__main__': app.run(debug=False,host='0.0.0.0')