diff --git a/store/.gitignore b/store/.gitignore index 5b6c0960..43c9a261 100644 --- a/store/.gitignore +++ b/store/.gitignore @@ -1 +1,2 @@ config.toml +.stars diff --git a/store/app.py b/store/app.py index e5b22107..d2a183d5 100644 --- a/store/app.py +++ b/store/app.py @@ -1,3 +1,4 @@ +import subprocess import pycmarkgfm import time import re @@ -30,11 +31,13 @@ except Exception as e: mandatory_config_keys = [ "DISCOURSE_SSO_SECRET", "DISCOURSE_SSO_ENDPOINT", + "COOKIE_SECRET", "CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE", "GITHUB_LOGIN", "GITHUB_TOKEN", "GITHUB_EMAIL", "APPS_CACHE", + "STARS_DB_FOLDER", ] for key in mandatory_config_keys: @@ -70,8 +73,23 @@ for id_, category in catalog['categories'].items(): wishlist = toml.load(open("../wishlist.toml")) # This is the secret key used for session signing -app.secret_key = ''.join([str(random.randint(0, 9)) for i in range(99)]) +app.secret_key = config["COOKIE_SECRET"] +def get_stars(): + checksum = subprocess.check_output("find . -type f -printf '%T@,' | md5sum", shell=True).decode().split()[0] + if get_stars.cache_checksum != checksum: + stars = {} + for folder, _, files in os.walk(config["STARS_DB_FOLDER"]): + app_id = folder.split("/")[-1] + if not app_id: + continue + stars[app_id] = set(files) + get_stars.cache_stars = stars + get_stars.cache_checksum = checksum + + return get_stars.cache_stars +get_stars.cache_checksum = None +get_stars() def human_to_binary(size: str) -> int: symbols = ("K", "M", "G", "T", "P", "E", "Z", "Y") @@ -110,6 +128,7 @@ def login_using_discourse(): session.clear() session["nonce"] = nonce + print(f"DEBUG: none = {nonce}") return redirect(url) @@ -118,6 +137,8 @@ def login_using_discourse(): def sso_login_callback(): response = base64.b64decode(request.args['sso'].encode()).decode() user_data = urllib.parse.parse_qs(response) + print("DEBUG: nonce from url args " + user_data['nonce'][0]) + print("DEBUG: nonce from session args " + session.get("nonce")) if user_data['nonce'][0] != session.get("nonce"): return "Invalid nonce", 401 else: @@ -143,7 +164,7 @@ def index(): @app.route('/catalog') def browse_catalog(): - return render_template("catalog.html", init_sort=request.args.get("sort"), init_search=request.args.get("search"), init_category=request.args.get("category"), user=session.get('user', {}), catalog=catalog, timestamp_now=int(time.time())) + return render_template("catalog.html", init_sort=request.args.get("sort"), init_search=request.args.get("search"), init_category=request.args.get("category"), init_starsonly=request.args.get("starsonly"), user=session.get('user', {}), catalog=catalog, timestamp_now=int(time.time()), stars=get_stars()) @app.route('/app/') @@ -194,12 +215,39 @@ def app_info(app_id): ram_build_requirement = infos["manifest"]["integration"]["ram"]["build"] infos["manifest"]["integration"]["ram"]["build_binary"] = human_to_binary(ram_build_requirement) - return render_template("app.html", user=session.get('user', {}), app_id=app_id, infos=infos, catalog=catalog) + return render_template("app.html", user=session.get('user', {}), app_id=app_id, infos=infos, catalog=catalog, stars=get_stars()) +@app.route('/app//') +def star_app(app_id, action): + assert action in ["star", "unstar"] + if app_id not in catalog["apps"] and app_id not in wishlist: + return f"App {app_id} not found", 404 + if not session.get('user', {}): + return f"You must be logged in to be able to star an app", 401 + + app_star_folder = os.path.join(config["STARS_DB_FOLDER"], app_id) + app_star_for_this_user = os.path.join(config["STARS_DB_FOLDER"], app_id, session.get('user', {})["id"]) + + if not os.path.exists(app_star_folder): + os.mkdir(app_star_folder) + + if action == "star": + open(app_star_for_this_user, "w").write("") + elif action == "unstar": + try: + os.remove(app_star_for_this_user) + except FileNotFoundError: + pass + + if app_id in catalog["apps"]: + return redirect(f"/app/{app_id}") + else: + return redirect("/wishlist") + @app.route('/wishlist') def browse_wishlist(): - return render_template("wishlist.html", user=session.get('user', {}), wishlist=wishlist) + return render_template("wishlist.html", user=session.get('user', {}), wishlist=wishlist, stars=get_stars()) @app.route('/wishlist/add', methods=['GET', 'POST']) diff --git a/store/config.toml.example b/store/config.toml.example index b029ef74..1e5f931a 100644 --- a/store/config.toml.example +++ b/store/config.toml.example @@ -1,8 +1,10 @@ +COOKIE_SECRET = "abcdefghijklmnopqrstuvwxyz1234567890" # This secret is configured in Discourse DISCOURSE_SSO_SECRET = "abcdefghijklmnopqrstuvwxyz1234567890" DISCOURSE_SSO_ENDPOINT = "https://forum.yunohost.org/session/sso_provider" CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE = "http://localhost:5000/sso_login_callback" -DEBUG = false GITHUB_LOGIN = "yunohost-bot" GITHUB_EMAIL = "yunohost [at] yunohost.org" # Replace the [at] by actual @ GITHUB_TOKEN = "superSecretToken" +APPS_CACHE = "../.apps_cache/" +STARS_DB_FOLDER = ".stars/" diff --git a/store/templates/app.html b/store/templates/app.html index a6c54203..e04c1c54 100644 --- a/store/templates/app.html +++ b/store/templates/app.html @@ -35,12 +35,29 @@ {% endif %}
-
@@ -151,7 +161,7 @@ Applications currently broken or low-quality

- There are apps which failed our automatic tests.
+ These are apps which failed our automatic tests.
This is usually a temporary situation which requires packagers to fix something in the app.

@@ -172,6 +182,7 @@ let searchInput = document.getElementById('search'); let selectCategory = document.getElementById('selectcategory'); let selectSort = document.getElementById('selectsort'); + let toggleStarsonly = document.getElementById('starsonly'); function liveSearch() { // Locate the card elements @@ -179,12 +190,14 @@ // Locate the search input let search_query = searchInput.value.trim().toLowerCase(); let selectedCategory = selectCategory.value.trim(); + let starsOnly = toggleStarsonly.checked; let at_least_one_match = false; // Loop through the entries for (var i = 0; i < entries.length; i++) { // If the text is within the card and the text matches the search query if ((entries[i].textContent.toLowerCase().includes(search_query)) - && (! selectedCategory || (entries[i].dataset.category == selectedCategory))) + && (! selectedCategory || (entries[i].dataset.category == selectedCategory)) + && (! starsOnly || (entries[i].dataset.starred == "True"))) { // ...remove the `.is-hidden` class. entries[i].classList.remove("hidden"); @@ -220,6 +233,11 @@ return a.dataset.addedincatalog - b.dataset.addedincatalog; }); } + else if (sortBy === "popularity") { + toSort.sort(function(a, b) { + return a.dataset.stars < b.dataset.stars; + }); + } else if (sortBy === "") { toSort.sort(function(a, b) { return a.dataset.appid > b.dataset.appid; @@ -239,12 +257,14 @@ let search_query = searchInput.value.trim(); let category = selectCategory.value.trim(); let sortBy = selectSort.value.trim(); + let starsOnly = toggleStarsonly.checked; if ('URLSearchParams' in window) { var queryArgs = new URLSearchParams(window.location.search) if (search_query) { queryArgs.set("search", search_query) } else { queryArgs.delete("search"); }; if (category) { queryArgs.set("category", category) } else { queryArgs.delete("category"); }; if (sortBy) { queryArgs.set("sort", sortBy) } else { queryArgs.delete("sortBy"); }; + if (starsOnly) { queryArgs.set("starsonly", true) } else { queryArgs.delete("starsonly"); }; let newUrl; if (queryArgs.toString()) @@ -278,6 +298,11 @@ liveSort("catalogLowQuality"); }); + toggleStarsonly.addEventListener('change', () => { + clearTimeout(typingTimer); + typingTimer = setTimeout(liveSearch, typeInterval); + }); + liveSearch(); liveSort("catalogGoodQuality"); liveSort("catalogLowQuality"); diff --git a/store/templates/wishlist.html b/store/templates/wishlist.html index 23b32d15..c86f5a45 100644 --- a/store/templates/wishlist.html +++ b/store/templates/wishlist.html @@ -85,8 +85,9 @@ href="#" class="inline-block group btn-sm border text-violet-600 border-violet-500 hover:bg-violet-500 hover:text-white" > - 123 - + {{ stars.get(app, {})|length }} + +