From 3db73ec95dcface7519fcc19f01cf63971f18103 Mon Sep 17 00:00:00 2001
From: Alexandre Aubin
Date: Thu, 4 Jan 2024 01:57:18 +0100
Subject: [PATCH] store/wishlist_add: ratelimit wishlist proposal to once every
15 days per user
---
store/app.py | 12 ++++++++++--
store/templates/wishlist_add.html | 18 ++++++++++--------
store/utils.py | 23 ++++++++++++++++++++++-
3 files changed, 42 insertions(+), 11 deletions(-)
diff --git a/store/app.py b/store/app.py
index 7a2b6f30..f0863b9b 100644
--- a/store/app.py
+++ b/store/app.py
@@ -31,6 +31,8 @@ from utils import (
get_wishlist,
get_stars,
get_app_md_and_screenshots,
+ save_wishlist_submit_for_ratelimit,
+ check_wishlist_submit_ratelimit,
)
app = Flask(__name__, static_url_path="/assets", static_folder="assets")
@@ -199,7 +201,6 @@ def add_to_wishlist():
successmsg=None,
errormsg=errormsg,
)
-
csrf_token = request.form["csrf_token"]
if csrf_token != session.get("csrf_token"):
@@ -222,6 +223,10 @@ def add_to_wishlist():
boring_keywords_to_check_for_people_not_reading_the_instructions = ["free", "open source", "open-source", "self-hosted", "simple", "lightweight", "light-weight", "best", "most", "fast", "flexible", "puissante", "powerful", "secure"]
checks = [
+ (
+ check_wishlist_submit_ratelimit(session['user']['username']) is True,
+ _("Proposing wishlist additions is limited to once every 15 days per user.")
+ ),
(len(name) >= 3, _("App name should be at least 3 characters")),
(len(name) <= 30, _("App name should be less than 30 characters")),
(
@@ -258,7 +263,7 @@ def add_to_wishlist():
_("Please focus on what the app does, without using marketing, fuzzy terms, or repeating that the app is 'free' and 'self-hostable'.")
),
(
- description.lower().split()[0] != name an description.lower().split()[1] not in ["is", "est"],
+ description.lower().split()[0] != name and (len(description.split()) == 1 or description.lower().split()[1] not in ["is", "est"]),
_("No need to repeat '{app} is'. Focus on what the app does.")
)
]
@@ -369,6 +374,9 @@ Description: {description}
"Your proposed app has succesfully been submitted. It must now be validated by the YunoHost team. You can track progress here: %(url)s",
url=url,
)
+
+ save_wishlist_submit_for_ratelimit(session['user']['username'])
+
return render_template(
"wishlist_add.html",
locale=get_locale(),
diff --git a/store/templates/wishlist_add.html b/store/templates/wishlist_add.html
index 7fb2a561..29e6d54e 100644
--- a/store/templates/wishlist_add.html
+++ b/store/templates/wishlist_add.html
@@ -28,20 +28,22 @@
{{ _("You must first login to be allowed to submit an app to the wishlist") }}
+
+
{{ _("Note that, due to various abuses, we restricted login on the app store to 'trust level 1' users.
'Trust level 1' is obtained after interacting a minimum with the forum, and more specifically: entering at least 5 topics, reading at least 30 posts, and spending at least 10 minutes reading posts.") }}
- {% endif %}
-
-
-
-
- {{ _("Please check the license of the app your are proposing") }}
+ {% else %}
+
+
+
+ {{ _("Due to abuses, only one proposal every 15 days per user is allowed.") }}
-
- {{ _("The YunoHost project will only package free/open-source software (with possible case-by-case exceptions for apps which are not-totally-free)") }}
+
+ {{ _("Reviewing those proposals is tiring for volunteers, please don't yolo-send every random nerdy stuff you find on the Internet.") }}
+ {% endif %}
{% if errormsg %}
diff --git a/store/utils.py b/store/utils.py
index 5dcd302b..b7c3b85a 100644
--- a/store/utils.py
+++ b/store/utils.py
@@ -1,3 +1,4 @@
+import time
import base64
import os
import json
@@ -6,7 +7,7 @@ import subprocess
import pycmarkgfm
from emoji import emojize
from flask import request
-
+from hashlib import md5
AVAILABLE_LANGUAGES = ["en"] + os.listdir("translations")
@@ -92,6 +93,26 @@ def get_stars():
get_stars.cache_checksum = None
get_stars()
+def check_wishlist_submit_ratelimit(user):
+
+ dir_ = os.path.join(".wishlist_ratelimit")
+ if not os.path.exists(dir_):
+ os.mkdir(dir_)
+
+ f = os.path.join(dir_, md5(user.encode()).hexdigest())
+
+ return not os.path.exists(f) or (time.time() - os.path.getmtime(f)) > (15 * 24 * 3600) # 15 days
+
+def save_wishlist_submit_for_ratelimit(user):
+
+ dir_ = os.path.join(".wishlist_ratelimit")
+ if not os.path.exists(dir_):
+ os.mkdir(dir_)
+
+ f = os.path.join(dir_, md5(user.encode()).hexdigest())
+
+ open(f, "w").write("")
+
def human_to_binary(size: str) -> int:
symbols = ("K", "M", "G", "T", "P", "E", "Z", "Y")