diff --git a/docker-compose.yml b/docker-compose.yml index cdb7593..1b65f68 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,3 +9,14 @@ services: environment: - CONF_FOLDER=/bot/conf/ user: 1000:1000 + web: + build: . + command: python3 /bot/src/web.py + volumes: + - ./out:/bot/out + - ./conf:/bot/conf + environment: + - CONF_FOLDER=/bot/conf/ + user: 1000:1000 + ports: + - "5000:5000" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 41884de..c071ebe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ gplaycli==3.29 -python-telegram-bot==13.0 \ No newline at end of file +python-telegram-bot==13.0 +Flask==1.1.2 \ No newline at end of file diff --git a/src/download_utils.py b/src/download_utils.py index 9ba09ab..8c21721 100644 --- a/src/download_utils.py +++ b/src/download_utils.py @@ -53,15 +53,17 @@ class APK: if os.path.isfile(fname): self.file_name = fname - def check_dimension(self): - if os.path.getsize(self.file_name) > 50 * 1024 * 1023: + def check_dimension(self, split=True, remove=True): + if split and os.path.getsize(self.file_name) > 50 * 1024 * 1023: check_call(['split', '-b', '49M', self.file_name, self.file_name]) - os.remove(self.file_name) + if remove: + os.remove(self.file_name) return glob(escape(self.file_name) + '*') @contextmanager - def send(self): - files = self.check_dimension() # split if size >= 50MB + def send(self, split=True, remove=True): + files = self.check_dimension(split) # split if size >= 50MB yield files for f in files: # removing old files - os.remove(f) + if remove: + os.remove(f) diff --git a/src/static/main.css b/src/static/main.css new file mode 100644 index 0000000..2647288 --- /dev/null +++ b/src/static/main.css @@ -0,0 +1,194 @@ +/** + * Custom CSS + */ + a { + color: #0097A7; +} +.look-form{ + margin-top:20px; + margin-top:2rem; + margin-bottom:20px; + margin-bottom:2rem; + display: flex +} +.look-form label { + display:block; + font-weight:bold +} +.look-form input[type="text"] { + width:100%; + display:block; + border-radius:3px 0 0 3px; + border-radius:.3rem 0 0 .3rem; + color:#424242; + background:#fff; + border:2px solid #E0E0E0; + border:.17rem solid #E0E0E0; + transition:border-color ease-in-out .15s; + padding:11px 10px; + padding:1.1rem 1rem; +} +.look-form input[type="text"]:focus { + border-color:#0097A7; + outline:0 + box-shadow: 0 0 0 .2rem #0095A570; + -webkit-box-shadow: 0 0 0 .2rem #0095A570; +} +.look-form input[type="text"] { + font-family: monospace; +} +.look-form input[type="submit"] { + height: 4rem; + width: 7rem; + z-index: 2; + background-color: #0095A5; + color: #fff; + border-radius: 0 4px 4px 0; + border-radius: 0 .4rem .4rem 0; + border: 2px solid #14854f; + text-align: center; + -webkit-transition: opacity .2s ease; + transition: opacity .2s ease; + text-decoration: none; + cursor: pointer; +} +.look-form input[type="submit"]:hover { + background-color: #0095A5; + border-color: #14854f; + opacity:.85 +} +.look-form input[type="submit"]:active { + background-color: #0095A5; + border-color: #14854f; + opacity:.75 +} +.look-form.error input { + border-color:#F50057 +} +.console { + font-family: monospace; +} +.highlight { + color: #0097A7; +} +.look-segment { + border: 1px solid #E0E0E0; + border-top-color: rgb(224, 224, 224); + border-right-color: rgb(224, 224, 224); + border-bottom-color: rgb(224, 224, 224); + border-left-color: rgb(224, 224, 224); + border-radius: 3px; + border-radius: .3rem; + margin-top: 20px; + margin-top: 2rem; + margin-bottom: 20px; + margin-bottom: 2rem; + overflow: hidden; +} +.look-segment > .header { + background: #f5f5f5; + padding: 10px 20px; + padding: 1rem 2rem; + box-shadow: inset 0 -1px 0 #E0E0E0; +} +.look-segment > .body { + padding: 19px 20px; + padding: 1.9rem 2rem; + word-wrap: break-word; +} +.look-segment > .body > :last-child, .look-segment > .body > :last-child > :last-child { + margin-bottom: 0; +} +.look-segment > .body > :first-child, .look-segment > .body > :first-child > :first-child { + margin-top: 0; +} +.look-segment.amber { + border-color: #FFC107; +} +.look-segment.amber > .header { + box-shadow: inset 0 -1px 0 #FFC107; + background: #ffeeba; + color: #000; +} +.look-segment.green { + border-color: #4CAF50; +} +.look-segment.green > .header { + box-shadow: inset 0 -1px 0 #4CAF50; + background: #c7e7c8; + color: #000; +} +.look-label { + display: inline-block; + background: #0097A7; + color: #fff; + font-weight: bold; + border: 0; + border-radius: 3px; + border-radius: .3rem; + padding: 1px 4px; + padding: .1rem .4rem; + font-size: 14px; + text-align: center; + vertical-align: middle; + white-space: nowrap; + line-height: inherit; +} +.look-table { + width: 100%; +} +.look-table td, .look-table th { + padding: 10px; +} +.look-table a > code { + color: #0097A7; +} +.literal { + background: rgba(0,0,0,.075); + color: #000; +} +.faq .q { + font-style: italic; + color: #0077DD; +} +.legend { + font-weight: bolder; + font-size: 12px; +} +.legend .username { + color: blue; +} +.legend .password { + color: orange; +} +.legend .rounds, .legend .time_cost { + color: purple; +} +.legend .salt { + color: red; +} +.legend .hash { + color: green; +} +pre .username { + border-bottom: 2px solid blue; + padding-bottom: 4px; + /*background-color: #00008030;*/ +} +pre .password { + border-bottom: 2px solid orange; + padding-bottom: 4px; + /*background-color: #ffa50030;*/ +} +pre .rounds, pre .time_cost { + border-bottom: 2px solid purple; + background-color: #80008030; +} +pre .salt { + border-bottom: 2px solid red; + background-color: #ff000030; +} +pre .hash { + border-bottom: 2px solid green; + background-color: #00800030; +} \ No newline at end of file diff --git a/src/static/marx.min.css b/src/static/marx.min.css new file mode 100644 index 0000000..19a2f65 --- /dev/null +++ b/src/static/marx.min.css @@ -0,0 +1,2 @@ +*,:after,:before{-webkit-box-sizing:border-box;box-sizing:border-box}:after,:before{text-decoration:inherit;vertical-align:inherit}html{cursor:default;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;word-break:break-word}body{margin:0}h1{font-size:2em;margin:.67em 0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{margin:0}nav ol,nav ul{list-style:none;padding:0}pre{font-family:monospace,monospace;font-size:1em}abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}audio,canvas,iframe,img,svg,video{vertical-align:middle}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}iframe,img{border-style:none}svg:not([fill]){fill:currentColor}svg:not(:root){overflow:hidden}table{border-collapse:collapse}button,input,select{margin:0}button{overflow:visible;text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}fieldset{border:1px solid #a0a0a0;padding:.35em .75em .625em}input{overflow:visible}legend{color:inherit;display:table;max-width:100%;white-space:normal}progress{display:inline-block;vertical-align:baseline}select{text-transform:none}textarea{margin:0}[type=checkbox],[type=radio]{padding:0}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}::-webkit-input-placeholder{color:inherit;opacity:.54}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}::-moz-focus-inner{border-style:none;padding:0}:-moz-focusring{outline:1px dotted ButtonText}:-moz-ui-invalid{box-shadow:none}details,dialog{display:block}dialog{background-color:#fff;border:solid;color:#000;height:-moz-fit-content;height:-webkit-fit-content;height:fit-content;left:0;margin:auto;padding:1em;position:absolute;right:0;width:-moz-fit-content;width:-webkit-fit-content;width:fit-content}dialog:not([open]){display:none}summary{display:list-item}canvas{display:inline-block}template{display:none}[tabindex],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation;touch-action:manipulation}[hidden]{display:none}[aria-busy=true]{cursor:progress}[aria-controls]{cursor:pointer}[aria-disabled=true],[disabled]{cursor:not-allowed}[aria-hidden=false][hidden]{display:initial}[aria-hidden=false][hidden]:not(:focus){clip:rect(0,0,0,0);position:absolute}/*! Marx v3.0.6 - The classless CSS reset (perfect for Communists) | MIT License | https://github.com/mblode/marx */article,aside,details,footer,header,main,section,summary{margin:0 auto 16px;width:100%}main{display:block;margin:0 auto;max-width:768px;padding:0 16px 16px}footer{border-top:1px solid rgba(0,0,0,.12);padding:16px 0;text-align:center}footer p{margin-bottom:0}hr{border:0;border-top:1px solid rgba(0,0,0,.12);display:block;margin-top:16px;margin-bottom:16px;width:100%;-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}img{height:auto;max-width:100%;vertical-align:baseline}@media screen and (max-width:400px){article,aside,section{clear:both;display:block;max-width:100%}img{margin-right:16px}}embed,iframe,video{border:0}body{color:rgba(0,0,0,.8);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;font-size:16px;line-height:1.5}p{margin:0 0 16px}h1,h2,h3,h4,h5,h6{color:inherit;font-family:inherit;line-height:1.2;font-weight:500}h1{font-size:40px}h1,h2{margin:20px 0 16px}h2{font-size:32px}h3{font-size:28px}h3,h4{margin:16px 0 4px}h4{font-size:24px}h5{font-size:20px}h5,h6{margin:16px 0 4px}h6{font-size:16px}small{color:rgba(0,0,0,.54);vertical-align:bottom}pre{background:#f7f7f9;display:block;margin:16px 0;padding:16px;white-space:pre-wrap;overflow-wrap:break-word}code,pre{color:rgba(0,0,0,.8);font-family:Menlo,Monaco,Consolas,Courier New,monospace;font-size:16px}code{line-height:inherit;margin:0;padding:0;vertical-align:baseline;word-break:break-all;word-wrap:break-word}a{color:#007bff;text-decoration:none;background-color:rgba(0,0,0,0)}a:focus,a:hover{color:#0062cc;text-decoration:underline}dl{margin-bottom:16px}dd{margin-left:40px}ol,ul{margin-bottom:8px;padding-left:40px;vertical-align:baseline}blockquote{border-left:2px solid rgba(0,0,0,.8);font-style:italic;margin:16px 0;padding-left:16px}blockquote,figcaption{font-family:Georgia,Times,Times New Roman,serif}u{text-decoration:underline}s{text-decoration:line-through}sup{vertical-align:super}sub,sup{font-size:14px}sub{vertical-align:sub}mark{background:#ffeb3b}input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{background:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.12);border-radius:4px;color:rgba(0,0,0,.8);display:block;width:100%;font-size:1rem;padding:8px 16px;line-height:1.5;-webkit-transition:border-color .15s ease-in-out,-webkit-box-shadow .15s ease-in-out;transition:border-color .15s ease-in-out,-webkit-box-shadow .15s ease-in-out;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-box-shadow .15s ease-in-out;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol}input[type=color]{background:#fff;border:1px solid rgba(0,0,0,.12);border-radius:4px;display:inline-block;vertical-align:middle}input:not([type]){-webkit-appearance:none;background:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.12);border-radius:4px;color:rgba(0,0,0,.8);display:block;width:100%;padding:8px 16px;line-height:1.5;-webkit-transition:border-color .15s ease-in-out,-webkit-box-shadow .15s ease-in-out;transition:border-color .15s ease-in-out,-webkit-box-shadow .15s ease-in-out;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-box-shadow .15s ease-in-out;text-align:left}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus,select:focus,textarea:focus{background-color:#fff;border-color:#80bdff;outline:0;-webkit-box-shadow:0 0 0 .2rem rgba(0,123,255,.25);box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}input:not([type]):focus{background-color:#fff;border-color:#febf01;outline:0;-webkit-box-shadow:0 0 0 .2rem rgba(0,123,255,.25);box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:1px thin rgba(0,0,0,.12)}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled],select[disabled],textarea[disabled]{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.54);cursor:not-allowed;opacity:1}input:not([type])[disabled]{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.54);cursor:not-allowed;opacity:1}input[readonly],select[readonly],textarea[readonly]{border-color:rgba(0,0,0,.12);color:rgba(0,0,0,.54)}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{border-color:#ea1c0d;color:#f44336}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#f44336}select{border:1px solid rgba(0,0,0,.12);vertical-align:sub}select:not([size]):not([multiple]){height:-webkit-calc(2.25rem + 2px);height:calc(2.25rem + 2px)}select[multiple]{height:auto}label{display:inline-block;line-height:2}fieldset{border:0;margin:0;padding:8px 0}legend{border-bottom:1px solid rgba(0,0,0,.12);color:rgba(0,0,0,.8);display:block;margin-bottom:8px;padding:8px 0;width:100%}textarea{overflow:auto;resize:vertical}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0;display:inline}button,input[type=button],input[type=reset],input[type=submit]{background-color:#007bff;border:#007bff;border-radius:4px;color:#fff;padding:8px 16px;display:inline-block;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:1px solid rgba(0,0,0,0);font-size:1rem;line-height:1.5;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,-webkit-box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,-webkit-box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-box-shadow .15s ease-in-out}button::-moz-focus-inner,input[type=button]::-moz-focus-inner,input[type=reset]::-moz-focus-inner,input[type=submit]::-moz-focus-inner{padding:0}button:hover,input[type=button]:hover,input[type=reset]:hover,input[type=submit]:hover{background-color:#0069d9;border-color:#0062cc;color:#fff}button:not(:disabled):active,input[type=button]:not(:disabled):active,input[type=reset]:not(:disabled):active,input[type=submit]:not(:disabled):active{background-color:#0062cc;border-color:#005cbf;color:#fff}button:focus,input[type=button]:focus,input[type=reset]:focus,input[type=submit]:focus{outline:0;-webkit-box-shadow:0 0 0 .2rem rgba(0,123,255,.5);box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}button:disabled,input[type=button]:disabled,input[type=reset]:disabled,input[type=submit]:disabled{opacity:.65;cursor:not-allowed;background-color:#007bff;border-color:#007bff;color:#fff}table{border-top:1px solid rgba(0,0,0,.12);margin-bottom:16px}caption{padding:8px 0}thead th{border:0;border-bottom:2px solid rgba(0,0,0,.12);text-align:left}tr{margin-bottom:8px}td,th{border-bottom:1px solid rgba(0,0,0,.12);padding:16px;vertical-align:inherit}tfoot tr{text-align:left}tfoot td{color:rgba(0,0,0,.54);font-size:8px;font-style:italic;padding:16px 4px} +/*# sourceMappingURL=marx.min.css.map */ diff --git a/src/templates/error.html b/src/templates/error.html new file mode 100644 index 0000000..dedda39 --- /dev/null +++ b/src/templates/error.html @@ -0,0 +1,17 @@ + + + +Error + + + + +
+

apk-downloader

+

Error {{code}}

+

{{error}}

+
+

Return to the homepage or go back

+
+ + \ No newline at end of file diff --git a/src/templates/home.html b/src/templates/home.html new file mode 100644 index 0000000..5db0ceb --- /dev/null +++ b/src/templates/home.html @@ -0,0 +1,21 @@ + + + +apk-downloader + + + + +
+

apk-downloader

+

+ Download apk from the PlayStore
+
+

+ + +
+

+
+ + \ No newline at end of file diff --git a/src/web.py b/src/web.py new file mode 100644 index 0000000..3ce0441 --- /dev/null +++ b/src/web.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 + +import logging +import os + +from flask import Flask, request, render_template, redirect, url_for, session, send_file + +from download_utils import APK, BadPackageNameException +from subprocess import CalledProcessError + +if "LOG_LEVEL" in os.environ: + log_level = os.environ["LOG_LEVEL"] +else: + log_level = logging.INFO + +# Logging configuration. +logger = logging.getLogger(__name__) +logging.basicConfig( + format="%(asctime)s> [%(levelname)s] %(message)s", + datefmt="%d/%m/%Y %H:%M:%S", + level=log_level, +) +logging.getLogger("werkzeug").disabled = True + +if os.environ.get('CONF_FOLDER') is None: + logger.error("No conf folder available") + exit(1) +TOKEN = None +with open(os.path.join(os.environ['CONF_FOLDER'], 'web.token')) as f: + TOKEN = f.read().strip() + +CONF = { + 'yes': True, + 'verbose': False, + 'tokencachefile': os.path.join(os.environ['CONF_FOLDER'], 'token.cache'), +} +CONF_FILE = os.path.join(os.environ['CONF_FOLDER'], 'gplaycli.conf') + +app = Flask(__name__) +app.secret_key = TOKEN + +@app.errorhandler(400) +@app.errorhandler(404) +@app.errorhandler(500) +def application_error(error): + logger.error(error) + return render_template('error.html', code=error.code, error=str(error)), error.code + +@app.route("/return-file/") +def return_file(): + package_name = session.get("package_name") + filename = f"{package_name}.apk" + location = session.get("filename", '') + if location != '': + location = os.path.join('/bot', location) + logging.info(f"sending {location}") + return send_file(location, attachment_filename=filename, as_attachment=True) + +@app.route("/download/", methods=["GET"]) +def get_download(package_name): + return download(package_name) + +@app.route("/download/", methods=["POST"]) +def post_download(): + package_name = request.form.get('package_name', '').strip() + return download(package_name) + +def download(package_name): + logger.info("downloading {} ...".format(package_name)) + + apk = APK(package_name, conf=CONF, conf_file=CONF_FILE) + + try: + apk.download() + with apk.send(split=False, remove=False) as files: + for f in files: + session['package_name'] = package_name + session['filename'] = f + except BadPackageNameException as e: + logger.error("Bad package name: {}".format(e)) + return render_template('error.html', code=500, error="Bad package name: {}".format(e)), 500 + except CalledProcessError: + logger.error("Failed to send APK file") + return render_template('error.html', code=500, error="Failed to send APK file"), 500 + + return redirect(url_for("return_file")) + +@app.route("/", methods=["GET"], strict_slashes=False) +def home(): + return render_template('home.html') + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000)