diff --git a/files/assets/js/change_hole_modal.js b/files/assets/js/change_hole_modal.js new file mode 100644 index 000000000..db1151a63 --- /dev/null +++ b/files/assets/js/change_hole_modal.js @@ -0,0 +1,36 @@ +const hole_to = document.getElementById("hole_to") +const changeHoleButton = document.getElementById("changeHoleButton"); + +hole_to.addEventListener('keydown', (e) => { + if (!((e.ctrlKey || e.metaKey) && e.key === "Enter")) return; + + const targetDOM = document.activeElement; + if (!(targetDOM instanceof HTMLInputElement)) return; + + changeHoleButton.click() + bootstrap.Modal.getOrCreateInstance(document.getElementById('changeHoleModal')).hide() +}); + +function change_holeModal(id) { + changeHoleButton.disabled = false; + changeHoleButton.classList.remove('disabled'); + changeHoleButton.innerHTML='Change Hole'; + changeHoleButton.dataset.id = id + + hole_to.value = "" + setTimeout(() => { + hole_to.focus() + }, 500); +}; + +changeHoleButton.onclick = function() { + this.disabled = true; + this.classList.add('disabled'); + + postToast(this, '/change_hole/' + changeHoleButton.dataset.id, + { + "hole_to": hole_to.value + }, + () => {} + ); +} diff --git a/files/assets/js/report_post_modal.js b/files/assets/js/report_post_modal.js index 8068a85f5..8000802e8 100644 --- a/files/assets/js/report_post_modal.js +++ b/files/assets/js/report_post_modal.js @@ -24,7 +24,6 @@ function report_postModal(id) { }; reportPostButton.onclick = function() { - this.innerHTML='Reporting post'; this.disabled = true; this.classList.add('disabled'); diff --git a/files/classes/post.py b/files/classes/post.py index 2af97907b..664a12ac3 100644 --- a/files/classes/post.py +++ b/files/classes/post.py @@ -414,3 +414,10 @@ class Post(Base): @lazy def is_longpost(self): return len(self.body) >= 2000 + + @lazy + def hole_changable(self, v): + if self.hole == 'chudrama': + return v.admin_level >= PERMS['POST_COMMENT_MODERATION'] + else: + return v.admin_level >= PERMS['POST_COMMENT_MODERATION'] or (self.hole and v.mods_hole(hole_from)) or self.author_id == v.id diff --git a/files/helpers/config/holeaction_types.py b/files/helpers/config/holeaction_types.py index 2cbe2727f..374258155 100644 --- a/files/helpers/config/holeaction_types.py +++ b/files/helpers/config/holeaction_types.py @@ -24,7 +24,7 @@ HOLEACTION_TYPES = { "icon": 'fa-feather-alt', "color": 'bg-danger' }, - 'move_hole': { + 'change_hole': { "str": 'changed hole of {self.target_link}', "icon": 'fa-manhole', "color": 'bg-primary' diff --git a/files/helpers/config/modaction_types.py b/files/helpers/config/modaction_types.py index 9bdf0f7eb..d1075fd39 100644 --- a/files/helpers/config/modaction_types.py +++ b/files/helpers/config/modaction_types.py @@ -216,7 +216,7 @@ MODACTION_TYPES = { "icon": 'fa-memo', "color": 'bg-danger' }, - 'move_hole': { + 'change_hole': { "str": 'changed hole of {self.target_link}', "icon": 'fa-manhole', "color": 'bg-primary' diff --git a/files/routes/holes.py b/files/routes/holes.py index e026d31c1..c34ddfbf6 100644 --- a/files/routes/holes.py +++ b/files/routes/holes.py @@ -1005,3 +1005,98 @@ def post_hole_snappy_quotes(v, hole): g.db.add(ma) return {"message": "Snappy quotes edited successfully!"} + + +@app.post("/change_hole/") +@limiter.limit('1/second', scope=rpath) +@limiter.limit('1/second', scope=rpath, key_func=get_ID) +@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400) +@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400, key_func=get_ID) +@auth_required +def change_hole(pid, v): + post = get_post(pid) + + if post.ghost: + abort(403, "You can't move ghost posts into holes!") + + hole_from = post.hole + + hole_to = request.values.get("hole_to", "").strip() + hole_to = get_hole(hole_to, graceful=True) + hole_to = hole_to.name if hole_to else None + + if not post.hole_changable(v): + return False + + if hole_to == None: + if HOLE_REQUIRED: + abort(403, "All posts are required to be in holes!") + hole_to_in_notif = 'the main feed' + else: + hole_to_in_notif = f'/h/{hole_to}' + + if hole_from == hole_to: + abort(409, f"Post is already in {hole_to_in_notif}") + + if post.author.exiler_username(hole_to): + abort(403, f"User is exiled from this hole!") + + if hole_to == 'changelog': + abort(403, "/h/changelog is archived!") + + if hole_to in {'furry','vampire','racist','femboy','edgy'} and not v.client and not post.author.house.lower().startswith(hole_to): + if v.id == post.author_id: + abort(403, f"You need to be a member of House {hole_to.capitalize()} to post in /h/{hole_to}") + else: + abort(403, f"@{post.author_name} needs to be a member of House {hole_to.capitalize()} for their post to be moved to /h/{hole_to}") + + post.hole = hole_to + post.hole_pinned = None + + if hole_to == 'chudrama': + post.bannedfor = None + post.chuddedfor = None + for c in post.comments: + c.bannedfor = None + c.chuddedfor = None + g.db.add(c) + + g.db.add(post) + + if v.id != post.author_id: + hole_from_str = 'main feed' if hole_from is None else \ + f'/h/{hole_from}' + hole_to_str = 'main feed' if hole_to is None else \ + f'/h/{hole_to}' + + if v.admin_level >= PERMS['POST_COMMENT_MODERATION']: + ma = ModAction( + kind='change_hole', + user_id=v.id, + target_post_id=post.id, + _note=f'{hole_from_str} → {hole_to_str}', + ) + g.db.add(ma) + position = 'a site admin' + else: + ma = HoleAction( + hole=hole_from, + kind='change_hole', + user_id=v.id, + target_post_id=post.id, + _note=f'{hole_from_str} → {hole_to_str}', + ) + g.db.add(ma) + position = f'a /h/{hole_from} mod' + + if hole_from == None: + hole_from_in_notif = 'the main feed' + else: + hole_from_in_notif = f'/h/{hole_from}' + + message = f"@{v.username} ({position}) has moved [{post.title}]({post.shortlink}) from {hole_from_in_notif} to {hole_to_in_notif}" + send_repeatable_notification(post.author_id, message) + + cache.delete_memoized(frontlist) + + return {"message": f"Post moved to {hole_to_in_notif} successfully!"} diff --git a/files/routes/jinja2.py b/files/routes/jinja2.py index df9300378..c99ebeb6d 100644 --- a/files/routes/jinja2.py +++ b/files/routes/jinja2.py @@ -150,6 +150,15 @@ def poster_of_the_day(): user = g.db.query(User).filter_by(id=uid).one() return user + +def HOLES(): + HOLES = [x[0] for x in g.db.query(Hole.name).order_by(Hole.name)] + if "other" in HOLES: + HOLES.remove("other") + HOLES.append("other") + return HOLES + + @app.context_processor def inject_constants(): return { @@ -177,7 +186,7 @@ def inject_constants(): "SITE_FULL_IMAGES": SITE_FULL_IMAGES, "IS_EVENT":IS_EVENT, "IS_FISTMAS":IS_FISTMAS, "IS_HOMOWEEN":IS_HOMOWEEN, "IS_DKD":IS_DKD, "IS_BIRTHGAY":IS_BIRTHGAY, "IS_BIRTHDEAD":IS_BIRTHDEAD, - "CHUD_PHRASES":CHUD_PHRASES, "hasattr":hasattr, "calc_users":calc_users, "HOLE_INACTIVITY_DELETION":HOLE_INACTIVITY_DELETION, "LIGHT_THEMES":LIGHT_THEMES, "NSFW_EMOJIS":NSFW_EMOJIS, + "CHUD_PHRASES":CHUD_PHRASES, "hasattr":hasattr, "calc_users":calc_users, "HOLE_INACTIVITY_DELETION":HOLE_INACTIVITY_DELETION, "LIGHT_THEMES":LIGHT_THEMES, "NSFW_EMOJIS":NSFW_EMOJIS, "HOLES":HOLES, "MAX_IMAGE_AUDIO_SIZE_MB":MAX_IMAGE_AUDIO_SIZE_MB, "MAX_IMAGE_AUDIO_SIZE_MB_PATRON":MAX_IMAGE_AUDIO_SIZE_MB_PATRON, "MAX_VIDEO_SIZE_MB":MAX_VIDEO_SIZE_MB, "MAX_VIDEO_SIZE_MB_PATRON":MAX_VIDEO_SIZE_MB_PATRON, "CURSORMARSEY_DEFAULT":CURSORMARSEY_DEFAULT, "SNAPPY_ID":SNAPPY_ID, "get_running_orgy":get_running_orgy, diff --git a/files/routes/posts.py b/files/routes/posts.py index 18a22ecd8..ed5e68b88 100644 --- a/files/routes/posts.py +++ b/files/routes/posts.py @@ -96,15 +96,10 @@ def publish(pid, v): @auth_required def submit_get(v, hole=None): hole = get_hole(hole, graceful=True) + if request.path.startswith('/h/') and not hole: abort(404) - HOLES = [x[0] for x in g.db.query(Hole.name).order_by(Hole.name)] - - if "other" in HOLES: - HOLES.remove("other") - HOLES.append("other") - - return render_template("submit.html", HOLES=HOLES, v=v, hole=hole) + return render_template("submit.html", v=v, hole=hole) @app.get("/post/") @app.get("/post//") diff --git a/files/routes/reporting.py b/files/routes/reporting.py index 36897f96f..9172cda95 100644 --- a/files/routes/reporting.py +++ b/files/routes/reporting.py @@ -60,9 +60,6 @@ def report_post(pid, v): return {"message": "Post flaired successfully!"} - moved = move_post(post, v, reason) - if moved: return {"message": moved} - if v.is_muted: abort(403, "You are forbidden from making reports!") existing = g.db.query(Report.post_id).filter_by(user_id=v.id, post_id=post.id).one_or_none() @@ -162,91 +159,3 @@ def remove_report_comment(v, cid, uid): g.db.add(ma) return {"message": "Report removed successfully!"} - -def move_post(post, v, reason): - if not reason.startswith('/h/') and not reason.startswith('h/'): - return False - - if post.ghost: - abort(403, "You can't move ghost posts into holes!") - - hole_from = post.hole - hole_to = get_hole(reason, graceful=True) - hole_to = hole_to.name if hole_to else None - - can_move_post = v.admin_level >= PERMS['POST_COMMENT_MODERATION'] or (post.hole and v.mods_hole(hole_from)) - if hole_from != 'chudrama': # posts can only be moved out of /h/chudrama by admins - can_move_post = can_move_post or post.author_id == v.id - if not can_move_post: return False - - if hole_to == None: - if HOLE_REQUIRED: - abort(403, "All posts are required to be flaired!") - hole_to_in_notif = 'the main feed' - else: - hole_to_in_notif = f'/h/{hole_to}' - - if hole_from == hole_to: abort(409, f"Post is already in {hole_to_in_notif}") - - if post.author.exiler_username(hole_to): - abort(403, f"User is exiled from this hole!") - - if hole_to == 'changelog': - abort(403, "/h/changelog is archived!") - - if hole_to in {'furry','vampire','racist','femboy','edgy'} and not v.client and not post.author.house.lower().startswith(hole_to): - if v.id == post.author_id: - abort(403, f"You need to be a member of House {hole_to.capitalize()} to post in /h/{hole_to}") - else: - abort(403, f"@{post.author_name} needs to be a member of House {hole_to.capitalize()} for their post to be moved to /h/{hole_to}") - - post.hole = hole_to - post.hole_pinned = None - - if hole_to == 'chudrama': - post.bannedfor = None - post.chuddedfor = None - for c in post.comments: - c.bannedfor = None - c.chuddedfor = None - g.db.add(c) - - g.db.add(post) - - if v.id != post.author_id: - hole_from_str = 'main feed' if hole_from is None else \ - f'/h/{hole_from}' - hole_to_str = 'main feed' if hole_to is None else \ - f'/h/{hole_to}' - - if v.admin_level >= PERMS['POST_COMMENT_MODERATION']: - ma = ModAction( - kind='move_hole', - user_id=v.id, - target_post_id=post.id, - _note=f'{hole_from_str} → {hole_to_str}', - ) - g.db.add(ma) - position = 'a site admin' - else: - ma = HoleAction( - hole=hole_from, - kind='move_hole', - user_id=v.id, - target_post_id=post.id, - _note=f'{hole_from_str} → {hole_to_str}', - ) - g.db.add(ma) - position = f'a /h/{hole_from} mod' - - if hole_from == None: - hole_from_in_notif = 'the main feed' - else: - hole_from_in_notif = f'/h/{hole_from}' - - message = f"@{v.username} ({position}) has moved [{post.title}]({post.shortlink}) from {hole_from_in_notif} to {hole_to_in_notif}" - send_repeatable_notification(post.author_id, message) - - cache.delete_memoized(frontlist) - - return f"Post moved to {hole_to_in_notif} successfully!" diff --git a/files/templates/modals/change_hole.html b/files/templates/modals/change_hole.html new file mode 100644 index 000000000..00b46ed91 --- /dev/null +++ b/files/templates/modals/change_hole.html @@ -0,0 +1,31 @@ + + + diff --git a/files/templates/post.html b/files/templates/post.html index 491de5d6d..2a6766a97 100644 --- a/files/templates/post.html +++ b/files/templates/post.html @@ -339,8 +339,15 @@ {% if v %} - {% if v.id == p.author_id %}{% include "modals/delete_post.html" %}{% endif %} + {% if v.id == p.author_id %} + {% include "modals/delete_post.html" %} + {% endif %} {% include "modals/report_post.html" %} + + {% if p.hole_changable(v) %} + {% include "modals/change_hole.html" %} + {% endif %} + {% if v.can_edit(p) %} {% endif %} diff --git a/files/templates/post_actions.html b/files/templates/post_actions.html index e9a8019e5..e1a739b5d 100644 --- a/files/templates/post_actions.html +++ b/files/templates/post_actions.html @@ -21,6 +21,10 @@ + + {% if p.hole_changable(v) %} + + {% endif %} {% endif %} {% if v and v.id == p.author_id %} diff --git a/files/templates/post_actions_mobile.html b/files/templates/post_actions_mobile.html index 0ddcb83a3..8b6da3c0d 100644 --- a/files/templates/post_actions_mobile.html +++ b/files/templates/post_actions_mobile.html @@ -15,6 +15,10 @@ +{% if p.hole_changable(v) %} + +{% endif %} + {% if FEATURES['AWARDS'] -%} {%- endif %} diff --git a/files/templates/post_listing.html b/files/templates/post_listing.html index d257c9b79..8f75e88c7 100644 --- a/files/templates/post_listing.html +++ b/files/templates/post_listing.html @@ -250,6 +250,7 @@ {% if v %} {% include "modals/delete_post.html" %} {% include "modals/report_post.html" %} + {% include "modals/change_hole.html" %} {% if v.admin_level >= PERMS['USER_BAN'] %} {% include "modals/punish.html" %} {% endif %} diff --git a/files/templates/submit.html b/files/templates/submit.html index 978aa457e..ce1e3a714 100644 --- a/files/templates/submit.html +++ b/files/templates/submit.html @@ -21,7 +21,7 @@ {%- set hole_placeholder = 'Required' if HOLE_REQUIRED else 'Optional' -%} - {% for h in HOLES %} + {% for h in HOLES() %} {% endfor %} diff --git a/migrations/20240214-add-btn-to-change-hole.sql b/migrations/20240214-add-btn-to-change-hole.sql new file mode 100644 index 000000000..dc443970f --- /dev/null +++ b/migrations/20240214-add-btn-to-change-hole.sql @@ -0,0 +1,2 @@ +update modactions set kind='change_hole' where kind='move_hole'; +update hole_actions set kind='change_hole' where kind='move_hole';