From 3cca37f8143b1dbf915dbf8cec7c544e1ebcfac9 Mon Sep 17 00:00:00 2001 From: csd4ni3l Date: Fri, 15 Aug 2025 15:56:11 +0200 Subject: [PATCH] Add achievements, profile and profile settings, add flash when accessing unathorized content --- constants.py | 14 ++ main.py | 203 +++++++++++++++++++++---- templates/achievements.jinja2 | 51 +++++++ templates/base.jinja2 | 2 +- templates/home.jinja2 | 6 + templates/leaderboard.jinja2 | 6 + templates/profile.jinja2 | 124 +++++++++++++++ templates/submit_grass_touching.jinja2 | 8 +- 8 files changed, 380 insertions(+), 34 deletions(-) create mode 100644 templates/achievements.jinja2 create mode 100644 templates/profile.jinja2 diff --git a/constants.py b/constants.py index 8510e7c..7e743bd 100644 --- a/constants.py +++ b/constants.py @@ -12,4 +12,18 @@ DATABASE_FILE = "data.db" MINIMUM_OCR_SIMILARITY = 0.7 OCR_CHALLENGE_LENGTH = 1 +ACHIEVEMENTS = [ + [1, "I went outside!", "Brag to your friends with this one! You went outside the first time in your life. Continue on your journey."], + [3, "Keeping up the streak!", "You went outside 3 times. Great job! (You should get one, btw)"], + [7, "Out for a week!", "7 days of breathing fresh air, you're practically a nature veteran."], + [14, "Two Weeks in the Wild", "Careful, you might be starting to get a tan."], + [30, "One with the Outdoors", "An entire month! Are you sure you’re still a gamer?"], + [50, "Grass Connoisseur", "You can now identify at least three different types of grass by touch alone."], + [100, "Master of Chlorophyll", "The grass respects you now."], + [200, "Photosynthesis Apprentice", "You spend so much time outside that plants start thinking you're one of them."], + [365, "Solar-Powered", "A whole year of going outside, you've unlocked infinite vitamin D."], + [500, "Grass Whisperer", "You hear the lawn speaking to you. It's weirdly supportive."], + [1000, "Legendary Lawn Treader", "Songs will be sung of your bravery and your sandals."], +] + UPLOAD_DIR = os.path.join(os.getcwd(), UPLOAD_DIR) \ No newline at end of file diff --git a/main.py b/main.py index 081a2e6..8d7f9b1 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,6 @@ from flask import Flask, redirect, url_for, render_template, request, Response, send_from_directory, g, flash from dotenv import load_dotenv -from constants import RICKROLL_LINK, UPLOAD_DIR, MINIMUM_COSINE_SIMILARITY, MINIMUM_OCR_SIMILARITY, DATABASE_FILE +from constants import RICKROLL_LINK, UPLOAD_DIR, MINIMUM_COSINE_SIMILARITY, MINIMUM_OCR_SIMILARITY, DATABASE_FILE, ACHIEVEMENTS from PIL import Image from jina import get_grass_touching_similarity @@ -22,6 +22,22 @@ challenges = {} os.makedirs("uploads", exist_ok=True) +def time_ago(seconds): + seconds = int(seconds) + if seconds < 60: + return f"{seconds} second{'s' if seconds != 1 else ''} ago" + elif seconds < 3600: + minutes = seconds // 60 + return f"{minutes} minute{'s' if minutes != 1 else ''} ago" + elif seconds < 86400: + hours = seconds // 3600 + return f"{hours} hour{'s' if hours != 1 else ''} ago" + else: + days = seconds // 86400 + return f"{days} day{'s' if days != 1 else ''} ago" + +app.jinja_env.filters['timeago'] = time_ago + def get_db(): db = getattr(g, '_database', None) if db is None: @@ -46,6 +62,37 @@ def get_db(): db.commit() return db +class User(flask_login.UserMixin): + pass + +@login_manager.user_loader +def user_loader(user_id): + user = User() + user.id = user_id + return user + +@login_manager.unauthorized_handler +def unauthorized_handler(): + flash("Why are you trying to access content reserved for logged in users? Just go outside, touch some grass and register your own account.", "error") + return redirect(url_for("login")) + +@app.before_request +def check_banned(): + if not hasattr(flask_login.current_user, "id"): + return + + username = flask_login.current_user.id + + cur = get_db().cursor() + cur.execute("SELECT banned FROM Users WHERE username = ?", (username,)) + row = cur.fetchone() + cur.close() + + if row is None or row[0]: + flash("Imagine forgetting to touch grass so you get banned from my app. Such a discord moderator you are. You have no life. Just go outside.", "error") + flask_login.logout_user() + return redirect("/") + @app.teardown_appcontext def close_connection(exception): db = getattr(g, '_database', None) @@ -65,40 +112,10 @@ def check_grass_touching_bans(): get_db().commit() cur.close() - time.sleep(60) + time.sleep(60) threading.Thread(target=check_grass_touching_bans, daemon=True).start() -class User(flask_login.UserMixin): - pass - -@login_manager.user_loader -def user_loader(user_id): - user = User() - user.id = user_id - return user - -@login_manager.unauthorized_handler -def unauthorized_handler(): - return redirect(url_for("login")) - -@app.before_request -def check_banned(): - if not hasattr(flask_login.current_user, "id"): - return - - username = flask_login.current_user.id - - cur = get_db().cursor() - cur.execute("SELECT banned FROM Users WHERE username = ?", (username,)) - row = cur.fetchone() - cur.close() - - if row is None or row[0]: - flash("Imagine forgetting to touch grass so you get banned from my app. Such a discord moderator you are. You have no life. Just go outside.", "error") - flask_login.logout_user() - return redirect("/") - def resize_image_file(path, max_side=256, fmt="JPEG"): img = Image.open(path) scale = max_side / max(img.size) @@ -220,6 +237,60 @@ def leaderboard(): return render_template("leaderboard.jinja2", users=users, current_username=username) +@app.route("/achievements") +@flask_login.login_required +def achievements(): + username = flask_login.current_user.id + + cur = get_db().cursor() + cur.execute("SELECT grass_touching_count FROM Users WHERE username = ?", (username,)) + + row = cur.fetchone() + + cur.close() + + if not row: + return Response("DB is not healthy.") + + return render_template("achievements.jinja2", achievements=ACHIEVEMENTS, grass_touching_count=row[0]) + +@app.route("/profile") +@flask_login.login_required +def profile(): + username = flask_login.current_user.id + + cur = get_db().cursor() + + cur.execute("SELECT grass_touching_count, last_grass_touch_time FROM Users WHERE username = ?", (username,)) + + row = cur.fetchone() + + cur.close() + + if not row: + return Response("DB is not healthy.") + + grass_touching_count, last_grass_touch_time = row + + return render_template("profile.jinja2", username=username, grass_touching_count=grass_touching_count, last_grass_touch_time=last_grass_touch_time, your_account=True) + +@app.route("/profile/") +def public_profile(username): + cur = get_db().cursor() + + cur.execute("SELECT grass_touching_count, last_grass_touch_time FROM Users WHERE username = ?", (username,)) + + row = cur.fetchone() + + cur.close() + + if not row: + return Response("DB is not healthy.") + + grass_touching_count, last_grass_touch_time = row + + return render_template("profile.jinja2", username=username, grass_touching_count=grass_touching_count, last_grass_touch_time=last_grass_touch_time, your_account=False) + @app.route("/login", methods=["GET", "POST"]) def login(): if request.method == "GET": @@ -280,7 +351,75 @@ def register(): cur.close() return redirect(url_for("login")) + +@app.route("/change_username", methods=["POST"]) +@flask_login.login_required +def change_username(): + username = flask_login.current_user.id + new_username = request.form["new_username"] + + cur = get_db().cursor() + + cur.execute("UPDATE Users SET username = ? WHERE username = ?", (new_username, username)) + cur.execute("UPDATE Images SET username = ? WHERE username = ?", (new_username, username)) + + get_db().commit() + cur.close() + + flask_login.logout_user() + return redirect(url_for("login")) + +@app.route("/change_password", methods=["POST"]) +@flask_login.login_required +def change_password(): + username = flask_login.current_user.id + new_password, confirm_password = request.form["new_password"], request.form["confirm_password"] + + if not secrets.compare_digest(new_password, confirm_password): + return Response("Passwords do not match.") + + cur = get_db().cursor() + + salt = bcrypt.gensalt() + hashed_password = bcrypt.hashpw(new_password.encode(), salt) + + cur.execute("UPDATE Users SET password = ?, password_salt = ? WHERE username = ?", (hashed_password, salt, username)) + get_db().commit() + cur.close() + + flask_login.logout_user() + return redirect(url_for("login")) + +@app.route("/delete_data", methods=["POST"]) +@flask_login.login_required +def delete_data(): + username = flask_login.current_user.id + + cur = get_db().cursor() + + cur.execute("DELETE FROM Users WHERE username = ?", (username,)) + + get_db().commit() + cur.close() + + flask_login.logout_user() + return redirect(url_for("login")) + +@app.route("/reset_data", methods=["POST"]) +@flask_login.login_required +def reset_data(): + username = flask_login.current_user.id + + cur = get_db().cursor() + + cur.execute("UPDATE Users SET last_grass_touch_time = ?, grass_touching_count = ? WHERE username = ?", (time.time(), 1, username)) + + get_db().commit() + cur.close() + + return redirect("/") + @app.route("/uploads/") def uploads(filename): return send_from_directory("uploads", filename) diff --git a/templates/achievements.jinja2 b/templates/achievements.jinja2 new file mode 100644 index 0000000..f3dc81e --- /dev/null +++ b/templates/achievements.jinja2 @@ -0,0 +1,51 @@ +{% extends "base.jinja2" %} +{% block title %}Grass Touching Achievements{% endblock %} + +{% block body %} + + +
+
+ {% for achievement in achievements %} + {% set unlocked = grass_touching_count >= achievement[0] %} +
+
+
+
+ {% if unlocked %} + βœ… {{ achievement[1] }} + {% else %} + πŸ”’ {{ achievement[1] }} + {% endif %} +
+
+ You have to go outside {{ achievement[0] }} times to get this! +
+

{{ achievement[2] }}

+
+
+
+ {% endfor %} +
+
+{% endblock %} diff --git a/templates/base.jinja2 b/templates/base.jinja2 index 065767b..95cff25 100644 --- a/templates/base.jinja2 +++ b/templates/base.jinja2 @@ -22,7 +22,7 @@