diff --git a/files/classes/hats.py b/files/classes/hats.py index 86da59d0a..a65739a4c 100644 --- a/files/classes/hats.py +++ b/files/classes/hats.py @@ -13,8 +13,10 @@ class HatDef(Base): description = Column(String) author_id = Column(Integer, ForeignKey('users.id')) price = Column(Integer) + submitter_id = Column(Integer, ForeignKey("users.id")) author = relationship("User", primaryjoin="HatDef.author_id == User.id", back_populates="designed_hats") + submitter = relationship("User", primaryjoin="HatDef.submitter_id == User.id") @property @lazy diff --git a/files/helpers/const.py b/files/helpers/const.py index b17011295..eac21edc4 100644 --- a/files/helpers/const.py +++ b/files/helpers/const.py @@ -212,7 +212,6 @@ SIDEBAR_THREAD = 0 BANNER_THREAD = 0 BADGE_THREAD = 0 SNAPPY_THREAD = 0 -HAT_THREAD = 0 if SITE == 'rdrama.net': FEATURES['PRONOUNS'] = True @@ -222,7 +221,6 @@ if SITE == 'rdrama.net': BANNER_THREAD = 37697 BADGE_THREAD = 37833 SNAPPY_THREAD = 37749 - HAT_THREAD = 100210 HOLE_COST = 50000 HOLE_INACTIVITY_DELETION = True @@ -941,7 +939,7 @@ if path.isfile(f'snappy_{SITE_NAME}.txt'): YOUTUBE_KEY = environ.get("YOUTUBE_KEY", "").strip() -ADMIGGERS = {SIDEBAR_THREAD, BANNER_THREAD, BADGE_THREAD, SNAPPY_THREAD, HAT_THREAD} +ADMIGGERS = {SIDEBAR_THREAD, BANNER_THREAD, BADGE_THREAD, SNAPPY_THREAD} proxies = {"http":"http://127.0.0.1:18080","https":"http://127.0.0.1:18080"} diff --git a/files/helpers/regex.py b/files/helpers/regex.py index 8e4b1abd2..8d50bca90 100644 --- a/files/helpers/regex.py +++ b/files/helpers/regex.py @@ -12,11 +12,12 @@ marseyaward_body_regex = re.compile(">[^<\s+]|[^>\s+]<", flags=re.A) marseyaward_title_regex = re.compile("( *]+>)+", flags=re.A) + marsey_regex = re.compile("marsey[a-z0-9]{1,24}", flags=re.A) - -hat_regex = re.compile("[a-zA-Z0-9\-() ,_]{1,50}", flags=re.A) - tags_regex = re.compile("[a-z0-9: ]{1,200}", flags=re.A) +hat_regex = re.compile("[a-zA-Z0-9\-() ,_]{1,50}", flags=re.A) +description_regex = re.compile("[^<>&\n\t]{1,300}", flags=re.A) + valid_sub_regex = re.compile("^[a-zA-Z0-9_\-]{3,25}$", flags=re.A) diff --git a/files/routes/comments.py b/files/routes/comments.py index b04efac1c..70783360f 100644 --- a/files/routes/comments.py +++ b/files/routes/comments.py @@ -231,51 +231,6 @@ def comment(v): data=f'{{"files": ["https://{SITE}/assets/images/badges/{badge.id}.webp"]}}', timeout=5) except Exception as e: return {"error": str(e)}, 400 - elif v.admin_level > 2 and parent_post.id == HAT_THREAD: - try: - hat = loads(body) - - name = hat["name"] - if not hat_regex.fullmatch(name): return {"error": "Invalid name!"}, 400 - existing = g.db.query(HatDef.name).filter_by(name=name).one_or_none() - if existing: return {"error": "A hat with this name already exists!"}, 403 - - if "author" in hat: user = get_user(hat["author"]) - elif "author_id" in hat: user = get_account(hat["author_id"]) - else: abort(400) - - filename = f'files/assets/images/hats/{name}.webp' - copyfile(oldname, filename) - process_image(filename, 200) - - hat_def = HatDef( - name=name, - description=hat["description"], - author_id=user.id, - price=hat["price"] - ) - g.db.add(hat_def) - g.db.flush() - - hat = Hat( - user_id=user.id, - hat_id=hat_def.id - ) - g.db.add(hat) - - all_by_author = g.db.query(HatDef).filter_by(author_id=user.id).count() - - if all_by_author >= 250: - badge_grant(badge_id=166, user=user) - elif all_by_author >= 100: - badge_grant(badge_id=165, user=user) - elif all_by_author >= 50: - badge_grant(badge_id=164, user=user) - elif all_by_author >= 10: - badge_grant(badge_id=163, user=user) - - except Exception as e: - return {"error": str(e)}, 400 body += f"\n\n![]({image})" elif file.content_type.startswith('video/'): body += f"\n\n{process_video(file)}" diff --git a/files/routes/static.py b/files/routes/static.py index 0c21f6e9d..84788bdca 100644 --- a/files/routes/static.py +++ b/files/routes/static.py @@ -441,6 +441,26 @@ def categories_json(): return jsonify(data) + + + + + + + + + +@app.get('/asset_submissions/') +@limiter.exempt +def asset_submissions(image): + if not image.endswith('.webp'): abort(404) + resp = make_response(send_from_directory('/asset_submissions', image)) + resp.headers.remove("Cache-Control") + resp.headers.add("Cache-Control", "public, max-age=3153600") + resp.headers.remove("Content-Type") + resp.headers.add("Content-Type", "image/webp") + return resp + @app.get("/submit/marseys") @auth_required def submit_marseys(v): @@ -526,7 +546,18 @@ def approve_marsey(v, name): if not tags: return {"error": "You need to include tags!"}, 400 - marsey.name = request.values.get('name').lower().strip() + new_name = request.values.get('name').lower().strip() + if not new_name: + return {"error": "You need to include name!"}, 400 + + + if not marsey_regex.fullmatch(new_name): + return {"error": "Invalid name!"}, 400 + if not tags_regex.fullmatch(tags): + return {"error": "Invalid tags!"}, 400 + + + marsey.name = new_name marsey.tags = tags g.db.add(marsey) @@ -546,11 +577,13 @@ def approve_marsey(v, name): data=f'{{"files": ["https://{SITE}/e/{marsey.name}.webp"]}}', timeout=5) cache.delete_memoized(marsey_list) - msg = f'@{v.username} has approved a marsey you submitted: :{marsey.name}:' - send_repeatable_notification(marsey.submitter_id, msg) + if v.id != marsey.submitter_id: + msg = f"@{v.username} has approved a marsey you submitted: :{marsey.name}:" + send_repeatable_notification(marsey.submitter_id, msg) + marsey.submitter_id = None - return {"message": f"{marsey.name} approved!"} + return {"message": f"'{marsey.name}' approved!"} @app.post("/admin/reject/marsey/") @admin_level_required(3) @@ -564,22 +597,158 @@ def reject_marsey(v, name): if not marsey: return {"error": f"This marsey '{name}' doesn't exist!"}, 404 - msg = f"@{v.username} has rejected a marsey you submitted: `'{marsey.name}'`" - send_repeatable_notification(marsey.submitter_id, msg) + if v.id != marsey.submitter_id: + msg = f"@{v.username} has rejected a marsey you submitted: `'{marsey.name}'`" + send_repeatable_notification(marsey.submitter_id, msg) g.db.delete(marsey) os.remove(f"/asset_submissions/{marsey.name}.webp") - return {"message": f"{marsey.name} rejected!"} + return {"message": f"'{marsey.name}' rejected!"} -@app.get('/asset_submissions/') -@limiter.exempt -def asset_submissions(image): - if not image.endswith('.webp'): abort(404) - resp = make_response(send_from_directory('/asset_submissions', image)) - resp.headers.remove("Cache-Control") - resp.headers.add("Cache-Control", "public, max-age=3153600") - resp.headers.remove("Content-Type") - resp.headers.add("Content-Type", "image/webp") - return resp \ No newline at end of file + + +@app.get("/submit/hats") +@auth_required +def submit_hats(v): + if v.admin_level > 2: hats = g.db.query(HatDef).filter(HatDef.submitter_id != None).all() + else: hats = g.db.query(HatDef).filter(HatDef.submitter_id == v.id).all() + return render_template("submit_hats.html", v=v, hats=hats) + + +@app.post("/submit/hats") +@auth_required +def submit_hat(v): + + def error(error): + if v.admin_level > 2: hats = g.db.query(HatDef).filter(HatDef.submitter_id != None).all() + else: hats = g.db.query(HatDef).filter(HatDef.submitter_id == v.id).all() + return render_template("submit_hats.html", v=v, hats=hats, error=error), 400 + + if request.headers.get("cf-ipcountry") == "T1": + return error("Image uploads are not allowed through TOR.") + + file = request.files["image"] + if not file or not file.content_type.startswith('image/'): + return error("You need to submit an image!") + + name = request.values.get('name').lower().strip() + if not hat_regex.fullmatch(name): + return error("Invalid name!") + + existing = g.db.query(HatDef.name).filter_by(name=name).one_or_none() + if existing: + return error("A hat with this name already exists!") + + description = request.values.get('description').lower().strip() + if not description_regex.fullmatch(description): + return error("Invalid description!") + + author = request.values.get('author').strip() + author = get_user(author) + + highquality = f'/asset_submissions/{name}.png' + file.save(highquality) + + i = Image.open(highquality) + if i.width > 100 or i.height > 130: + return error("Images must be 100x130") + + filename = f'/asset_submissions/{name}.webp' + copyfile(highquality, filename) + process_image(filename) + + hat = HatDef(name=name, author_id=author.id, description=description, price=500, submitter_id=v.id) + g.db.add(hat) + + g.db.commit() + + if v.admin_level > 2: hats = g.db.query(HatDef).filter(HatDef.submitter_id != None).all() + else: hats = g.db.query(HatDef).filter(HatDef.submitter_id == v.id).all() + return render_template("submit_hats.html", v=v, hats=hats, msg=f"'{name}' submitted successfully!") + + +@app.post("/admin/approve/hat/") +@admin_level_required(3) +def approve_hat(v, name): + if CARP_ID and v.id != CARP_ID: + return {"error": "Only Carp can approve hats!"}, 403 + + name = name.lower().strip() + + hat = g.db.query(HatDef).filter_by(name=name).one_or_none() + if not hat: + return {"error": f"This hat '{name}' doesn't exist!"}, 404 + + description = request.values.get('description').lower().strip() + if not description: + return {"error": "You need to include description!"}, 400 + + new_name = request.values.get('name').lower().strip() + if not new_name: + return {"error": "You need to include name!"}, 400 + + if not hat_regex.fullmatch(new_name): + return {"error": "Invalid name!"}, 400 + + if not description_regex.fullmatch(description): + return {"error": "Invalid description!"}, 400 + + hat.name = new_name + hat.description = description + g.db.add(hat) + + move(f"/asset_submissions/{name}.webp", f"files/assets/images/hats/{hat.name}.webp") + + + g.db.flush() + author = hat.author + + all_by_author = g.db.query(HatDef).filter_by(author_id=author.id).count() + + if all_by_author >= 250: + badge_grant(badge_id=166, user=author) + elif all_by_author >= 100: + badge_grant(badge_id=165, user=author) + elif all_by_author >= 50: + badge_grant(badge_id=164, user=author) + elif all_by_author >= 10: + badge_grant(badge_id=163, user=author) + + hat_copy = Hat( + user_id=author.id, + hat_id=hat.id + ) + g.db.add(hat_copy) + + + + if v.id != hat.submitter_id: + msg = f"@{v.username} has approved a hat you submitted: '{hat.name}'" + send_repeatable_notification(hat.submitter_id, msg) + + hat.submitter_id = None + + return {"message": f"'{hat.name}' approved!"} + +@app.post("/admin/reject/hat/") +@admin_level_required(3) +def reject_hat(v, name): + if CARP_ID and v.id != CARP_ID: + return {"error": "Only Carp can reject hats!"}, 403 + + name = name.lower().strip() + + hat = g.db.query(HatDef).filter_by(name=name).one_or_none() + if not hat: + return {"error": f"This hat '{name}' doesn't exist!"}, 404 + + if v.id != hat.submitter_id: + msg = f"@{v.username} has rejected a hat you submitted: `'{hat.name}'`" + send_repeatable_notification(hat.submitter_id, msg) + + g.db.delete(hat) + os.remove(f"/asset_submissions/{hat.name}.webp") + + return {"message": f"'{hat.name}' rejected!"} \ No newline at end of file diff --git a/files/templates/megathread_index.html b/files/templates/megathread_index.html index 1bea03c5c..bbce3aa85 100644 --- a/files/templates/megathread_index.html +++ b/files/templates/megathread_index.html @@ -52,7 +52,7 @@ 'Submit Hats', 'Submit a Hat to be added.', 'fa-hat-cowboy', '#7c603e', - '/post/100209', + '/submit/hats', ) ])-%} {%- endif -%} diff --git a/files/templates/submit_hats.html b/files/templates/submit_hats.html new file mode 100644 index 000000000..a2cf58dbb --- /dev/null +++ b/files/templates/submit_hats.html @@ -0,0 +1,168 @@ +{% extends "default.html" %} + +{% block title %} +Submit Hats +{% endblock %} + +{% block pagetype %}message{% endblock %} + +{% block content %} + {% if error %} + + + + {{error}} + + + + + + {% endif %} + {% if msg %} + + + + {{msg}} + + + + + + {% endif %} + + + Submit Hat + + + + + + + + Image + + + + Select Image + + + + + Name + + + Author + + + Description + + + + + + + + + + + + Pending Carp Approval + + + + {% for hat in hats %} + + + + + + Image + + + Submitter + + + Author + + + Name + + + Description + + + Price + + + + {% if v.admin_level > 2 %} + + Reject + Approve + + {% endif %} + + {% endfor %} + + + +{% endblock %} diff --git a/files/templates/submit_marseys.html b/files/templates/submit_marseys.html index 7478e4dc4..fbbbcb6b3 100644 --- a/files/templates/submit_marseys.html +++ b/files/templates/submit_marseys.html @@ -143,23 +143,23 @@ Image - Name - - + Submitter + + Author - + + Name + + Tags - - Submitter - {% if v.admin_level > 2 %} - Reject - Approve + Reject + Approve {% endif %} diff --git a/sql/20220910 hat-submissin-ui.sql b/sql/20220910 hat-submissin-ui.sql new file mode 100644 index 000000000..d40bfbb18 --- /dev/null +++ b/sql/20220910 hat-submissin-ui.sql @@ -0,0 +1,6 @@ +alter table hat_defs add column submitter_id int; + +ALTER TABLE ONLY public.hat_defs + ADD CONSTRAINT hat_def_submitter_fkey FOREIGN KEY (submitter_id) REFERENCES public.users(id); + +CREATE INDEX hat_defs_submitter_id_idx ON public.hat_defs USING btree (submitter_id); \ No newline at end of file