diff --git a/files/assets/js/comments_v.js b/files/assets/js/comments_v.js index 49f17b490..70d2b9bbd 100644 --- a/files/assets/js/comments_v.js +++ b/files/assets/js/comments_v.js @@ -191,7 +191,7 @@ function comment_edit(id){ xhr[0].send(xhr[1]); } -function post_comment(fullname, hide){ +function post_comment(fullname, wall_user_id, hide){ const btn = document.getElementById('save-reply-to-'+fullname) btn.disabled = true btn.classList.add('disabled'); @@ -210,7 +210,9 @@ function post_comment(fullname, hide){ catch(e) {} const xhr = new XMLHttpRequest(); - xhr.open("post", "/comment"); + if (wall_user_id == 'None') url = "/comment" + else url = '/wall_comment' + xhr.open("post", url); xhr.setRequestHeader('xhr', 'xhr'); xhr.onload=function(){ let data diff --git a/files/classes/comment.py b/files/classes/comment.py index 41f037cb0..eaf03c869 100644 --- a/files/classes/comment.py +++ b/files/classes/comment.py @@ -33,6 +33,7 @@ class Comment(Base): id = Column(Integer, primary_key=True) author_id = Column(Integer, ForeignKey("users.id")) parent_submission = Column(Integer, ForeignKey("submissions.id")) + wall_user_id = Column(Integer, ForeignKey("users.id")) created_utc = Column(Integer) edited_utc = Column(Integer, default=0) is_banned = Column(Boolean, default=False) @@ -74,6 +75,7 @@ class Comment(Base): flags = relationship("CommentFlag", order_by="CommentFlag.created_utc") options = relationship("CommentOption", order_by="CommentOption.id") casino_game = relationship("Casino_Game") + wall_user = relationship("User", primaryjoin="User.id==Comment.wall_user_id") def __init__(self, *args, **kwargs): if "created_utc" not in kwargs: diff --git a/files/classes/user.py b/files/classes/user.py index 81ebf97cc..63c183b12 100644 --- a/files/classes/user.py +++ b/files/classes/user.py @@ -496,7 +496,7 @@ class User(Base): @property @lazy def fullname(self): - return f"t1_{self.id}" + return f"u_{self.id}" @property @lazy diff --git a/files/routes/comments.py b/files/routes/comments.py index 7ad0944ba..79d0b3f01 100644 --- a/files/routes/comments.py +++ b/files/routes/comments.py @@ -215,7 +215,7 @@ def comment(v): parent_submission=parent_post.id, parent_comment_id=parent_comment_id, level=level, - over_18=parent_post.over_18 or request.values.get("over_18")=="true", + over_18=parent_post.over_18, is_bot=is_bot, app_id=v.client.application.id if v.client else None, body_html=body_html, @@ -344,6 +344,241 @@ def comment(v): return {"comment": render_template("comments.html", v=v, comments=[c])} +#- API +@app.post("/wall_comment") +@limiter.limit("1/second;20/minute;200/hour;1000/day") +@auth_required +@ratelimit_user("1/second;20/minute;200/hour;1000/day") +def wall_comment(v): + if v.is_suspended: abort(403, "You can't perform this action while banned.") + + parent_fullname = request.values.get("parent_fullname").strip() + if len(parent_fullname) < 3: abort(400) + id = parent_fullname[2:] + parent_comment_id = None + + if parent_fullname.startswith("u_"): + parent = get_account(id, v=v) + parent_user = parent + parent_author = parent + elif parent_fullname.startswith("c_"): + parent = get_comment(id, v=v) + if parent.deleted_utc != 0: abort(404) + parent_user = parent.wall_user + parent_comment_id = parent.id + parent_author = parent_user + else: abort(400) + + level = 1 if isinstance(parent, User) else parent.level + 1 + + # if not User.can_see(v, parent): abort(404) + + if level > COMMENT_MAX_DEPTH: abort(400, f"Max comment level is {COMMENT_MAX_DEPTH}") + + body = sanitize_raw_body(request.values.get("body", ""), False) + + if v.longpost and (len(body) < 280 or ' [](' in body or body.startswith('[](')): + abort(403, "You have to type more than 280 characters!") + elif v.bird and len(body) > 140: + abort(403, "You have to type less than 140 characters!") + + if not body and not request.files.get('file'): + abort(400, "You need to actually write something!") + + if v.admin_level < PERMS['POST_COMMENT_MODERATION'] and parent_author.any_block_exists(v): + abort(403, "You can't reply to users who have blocked you or users that you have blocked.") + + options = [] + for i in list(poll_regex.finditer(body))[:POLL_MAX_OPTIONS]: + options.append(i.group(1)) + body = body.replace(i.group(0), "") + + choices = [] + for i in list(choice_regex.finditer(body))[:POLL_MAX_OPTIONS]: + choices.append(i.group(1)) + body = body.replace(i.group(0), "") + + if request.files.get("file") and not g.is_tor: + files = request.files.getlist('file')[:4] + for file in files: + if file.content_type.startswith('image/'): + oldname = f'/images/{time.time()}'.replace('.','') + '.webp' + file.save(oldname) + image = process_image(oldname, v) + if image == "": abort(400, "Image upload failed") + if v.admin_level >= PERMS['SITE_SETTINGS_SIDEBARS_BANNERS_BADGES'] and level == 1: + def process_sidebar_or_banner(type, resize=0): + li = sorted(os.listdir(f'files/assets/images/{SITE_NAME}/{type}'), + key=lambda e: int(e.split('.webp')[0]))[-1] + num = int(li.split('.webp')[0]) + 1 + filename = f'files/assets/images/{SITE_NAME}/{type}/{num}.webp' + copyfile(oldname, filename) + process_image(filename, v, resize=resize) + body += f"\n\n![]({image})" + elif file.content_type.startswith('video/'): + body += f"\n\n{SITE_FULL}{process_video(file, v)}" + elif file.content_type.startswith('audio/'): + body += f"\n\n{SITE_FULL}{process_audio(file, v)}" + else: + abort(415) + + body = body.strip()[:COMMENT_BODY_LENGTH_LIMIT] + + body_for_sanitize = body + if v.owoify: + body_for_sanitize = owoify(body_for_sanitize) + if v.marsify: + body_for_sanitize = marsify(body_for_sanitize) + + torture = (v.agendaposter and not v.marseyawarded) + body_html = sanitize(body_for_sanitize, limit_pings=5, count_marseys=not v.marsify, torture=torture) + + if '!wordle' not in body.lower() and AGENDAPOSTER_PHRASE not in body.lower(): + existing = g.db.query(Comment.id).filter(Comment.author_id == v.id, + Comment.deleted_utc == 0, + Comment.parent_comment_id == parent_comment_id, + Comment.parent_submission == None, + Comment.body_html == body_html + ).first() + if existing: abort(409, f"You already made that comment: /comment/{existing.id}") + + is_bot = (v.client is not None + and v.id not in PRIVILEGED_USER_BOTS + or (SITE == 'pcmemes.net' and v.id == SNAPPY_ID)) + + execute_antispam_comment_check(body, v) + execute_antispam_duplicate_comment_check(v, body_html) + + if len(body_html) > COMMENT_BODY_HTML_LENGTH_LIMIT: abort(400) + + c = Comment(author_id=v.id, + wall_user_id=parent_user.id, + parent_comment_id=parent_comment_id, + level=level, + is_bot=is_bot, + app_id=v.client.application.id if v.client else None, + body_html=body_html, + body=body, + ) + + c.upvotes = 1 + g.db.add(c) + g.db.flush() + + execute_blackjack(v, c, c.body, "comment") + execute_under_siege(v, c, c.body, "comment") + + if c.level == 1: c.top_comment_id = c.id + else: c.top_comment_id = parent.top_comment_id + + for option in options: + body_html = filter_emojis_only(option) + if len(body_html) > 500: abort(400, "Poll option too long!") + option = CommentOption( + comment_id=c.id, + body_html=body_html, + exclusive=0 + ) + g.db.add(option) + + for choice in choices: + body_html = filter_emojis_only(choice) + if len(body_html) > 500: abort(400, "Poll option too long!") + choice = CommentOption( + comment_id=c.id, + body_html=body_html, + exclusive=1 + ) + g.db.add(choice) + + if v.agendaposter and not v.marseyawarded and AGENDAPOSTER_PHRASE not in c.body.lower(): + c.is_banned = True + c.ban_reason = "AutoJanny" + g.db.add(c) + + body = AGENDAPOSTER_MSG.format(username=v.username, type='comment', AGENDAPOSTER_PHRASE=AGENDAPOSTER_PHRASE) + body_jannied_html = AGENDAPOSTER_MSG_HTML.format(id=v.id, username=v.username, type='comment', AGENDAPOSTER_PHRASE=AGENDAPOSTER_PHRASE) + + c_jannied = Comment(author_id=AUTOJANNY_ID, + parent_submission=None, + wall_user_id=parent_user.id, + distinguish_level=6, + parent_comment_id=c.id, + level=level+1, + is_bot=True, + body=body, + body_html=body_jannied_html, + top_comment_id=c.top_comment_id, + ) + + g.db.add(c_jannied) + g.db.flush() + + n = Notification(comment_id=c_jannied.id, user_id=v.id) + g.db.add(n) + + if not v.shadowbanned: + notify_users = NOTIFY_USERS(body, v) + + if parent_author.id != v.id: + notify_users.add(parent_author.id) + + for x in notify_users-bots: + n = Notification(comment_id=c.id, user_id=x) + g.db.add(n) + + if VAPID_PUBLIC_KEY != DEFAULT_CONFIG_VALUE and parent_author.id != v.id and not v.shadowbanned: + title = f'New comment on your wall by @{c.author_name}' + + if len(c.body) > 500: notifbody = c.body[:500] + '...' + else: notifbody = c.body + + url = f'{SITE_FULL}/comment/{c.id}?context=8&read=true#context' + + push_notif(parent_author.id, title, notifbody, url) + + + + vote = CommentVote(user_id=v.id, + comment_id=c.id, + vote_type=1, + ) + + g.db.add(vote) + + + v.comment_count = g.db.query(Comment).filter( + Comment.author_id == v.id, + Comment.parent_submission != None, + Comment.deleted_utc == 0 + ).count() + g.db.add(v) + + c.voted = 1 + + if v.marseyawarded and marseyaward_body_regex.search(body_html): + abort(403, "You can only type marseys!") + + check_for_treasure(body, c) + + if FEATURES['WORDLE'] and "!wordle" in body: + answer = random.choice(WORDLE_LIST) + c.wordle_result = f'_active_{answer}' + + check_slots_command(v, v, c) + + if c.level > 5: + n = g.db.query(Notification).filter_by( + comment_id=c.parent_comment.parent_comment.parent_comment.parent_comment_id, + user_id=c.parent_comment.author_id, + ).one_or_none() + if n: g.db.delete(n) + + g.db.flush() + + if v.client: return c.json(db=g.db) + return {"comment": render_template("comments.html", v=v, comments=[c])} + @app.post("/edit_comment/") @limiter.limit("1/second;10/minute;100/hour;200/day") diff --git a/files/routes/users.py b/files/routes/users.py index 73c155d79..483b05cc5 100644 --- a/files/routes/users.py +++ b/files/routes/users.py @@ -714,6 +714,50 @@ def userpagelisting(user:User, site=None, v=None, page:int=1, sort="new", t="all @app.get("/@") @app.get("/@.json") @auth_desired_with_logingate +def u_username_wall(username, v=None): + u = get_user(username, v=v, include_blocks=True, include_shadowbanned=False) + if username != u.username: + return redirect(f"/@{u.username}/comments") + is_following = v and u.has_follower(v) + + if not u.is_visible_to(v): + if g.is_api_or_xhr or request.path.endswith(".json"): + abort(403, f"@{u.username}'s userpage is private") + return render_template("userpage/private.html", u=u, v=v, is_following=is_following), 403 + + if v and hasattr(u, 'is_blocking') and u.is_blocking: + if g.is_api_or_xhr or request.path.endswith(".json"): + abort(403, f"You are blocking @{u.username}.") + return render_template("userpage/blocking.html", u=u, v=v), 403 + + try: page = max(int(request.values.get("page", "1")), 1) + except: page = 1 + + comments, output = get_comments_v_properties(v, True, None, Comment.wall_user_id == u.id) + comments = comments.filter(Comment.level == 1) + + if not v or (v.id != u.id and v.admin_level < PERMS['POST_COMMENT_MODERATION']): + comments = comments.filter( + Comment.is_banned == False, + Comment.ghost == False, + Comment.deleted_utc == 0 + ) + + comments = comments.order_by(Comment.created_utc.desc()).offset(PAGE_SIZE * (page - 1)).limit(PAGE_SIZE+1) + comments = [c[0] for c in comments.all()] + + next_exists = (len(comments) > PAGE_SIZE) + comments = comments[:PAGE_SIZE] + + if (v and v.client) or request.path.endswith(".json"): + return {"data": [c.json(g.db) for c in comments]} + + return render_template("userpage/wall.html", u=u, v=v, listing=comments, page=page, next_exists=next_exists, is_following=is_following, standalone=True, render_replies=True) + + +@app.get("/@/posts") +@app.get("/@/posts.json") +@auth_desired_with_logingate def u_username(username, v=None): u = get_user(username, v=v, include_blocks=True, include_shadowbanned=False) if username != u.username: @@ -766,7 +810,7 @@ def u_username(username, v=None): if (v and v.client) or request.path.endswith(".json"): return {"data": [x.json(g.db) for x in listing]} - return render_template("userpage.html", + return render_template("userpage/userpage.html", unban=u.unban_string, u=u, v=v, @@ -780,7 +824,7 @@ def u_username(username, v=None): if (v and v.client) or request.path.endswith(".json"): return {"data": [x.json(g.db) for x in listing]} - return render_template("userpage.html", + return render_template("userpage/userpage.html", u=u, v=v, listing=listing, @@ -1005,7 +1049,7 @@ def saved_posts(v:User, username): try: page = max(1, int(request.values.get("page", 1))) except: abort(400, "Invalid page input!") - return get_saves_and_subscribes(v, "userpage.html", SaveRelationship, page, False) + return get_saves_and_subscribes(v, "userpage/userpage.html", SaveRelationship, page, False) @app.get("/@/saved/comments") @auth_required @@ -1021,7 +1065,7 @@ def subscribed_posts(v:User, username): try: page = max(1, int(request.values.get("page", 1))) except: abort(400, "Invalid page input!") - return get_saves_and_subscribes(v, "userpage.html", Subscription, page, False) + return get_saves_and_subscribes(v, "userpage/userpage.html", Subscription, page, False) @app.post("/fp/") @auth_required diff --git a/files/routes/votes.py b/files/routes/votes.py index d52a4e696..95ecf095e 100644 --- a/files/routes/votes.py +++ b/files/routes/votes.py @@ -71,7 +71,7 @@ def vote_post_comment(target_id, new, v, cls, vote_cls): target = get_post(target_id) elif cls == Comment: target = get_comment(target_id) - if not target.parent_submission: abort(404) + if not target.parent_submission and not target.wall_user_id: abort(404) else: abort(404) diff --git a/files/templates/admin/image_posts.html b/files/templates/admin/image_posts.html index d98a42964..42b94b4c6 100644 --- a/files/templates/admin/image_posts.html +++ b/files/templates/admin/image_posts.html @@ -1,4 +1,4 @@ -{% extends "userpage.html" %} +{% extends "userpage/userpage.html" %} {% block pagetype %}userpage{% endblock %} {% block desktopBanner %}{% endblock %} {% block desktopUserBanner %}{% endblock %} diff --git a/files/templates/admin/removed_posts.html b/files/templates/admin/removed_posts.html index cb6ec9704..f2f0543d1 100644 --- a/files/templates/admin/removed_posts.html +++ b/files/templates/admin/removed_posts.html @@ -1,4 +1,4 @@ -{% extends "userpage.html" %} +{% extends "userpage/userpage.html" %} {% block pagetype %}userpage{% endblock %} {% block desktopBanner %}{% endblock %} {% block desktopUserBanner %}{% endblock %} diff --git a/files/templates/admin/reported_posts.html b/files/templates/admin/reported_posts.html index 2531b0dd5..2492194f0 100644 --- a/files/templates/admin/reported_posts.html +++ b/files/templates/admin/reported_posts.html @@ -1,4 +1,4 @@ -{% extends "userpage.html" %} +{% extends "userpage/userpage.html" %} {% block pagetype %}userpage{% endblock %} {% block desktopBanner %}{% endblock %} {% block desktopUserBanner %}{% endblock %} diff --git a/files/templates/comments.html b/files/templates/comments.html index 814a701fe..ee2d48051 100644 --- a/files/templates/comments.html +++ b/files/templates/comments.html @@ -92,7 +92,7 @@ {% endif %} {% elif c.author_id==AUTOJANNY_ID %} Notification - {% else %} + {% elif not c.wall_user_id %} {% if c.sentto == MODMAIL_ID %} Sent to admins {% else %} @@ -266,7 +266,7 @@
{{c.realbody(v) | safe}}
- {% if c.parent_submission %} + {% if c.parent_submission or c.wall_user_id %} {% if v and v.id==c.author_id %} - +
@@ -696,7 +696,7 @@ - +
diff --git a/files/templates/userpage/comments.html b/files/templates/userpage/comments.html index 335229570..9f22e657c 100644 --- a/files/templates/userpage/comments.html +++ b/files/templates/userpage/comments.html @@ -1,4 +1,4 @@ -{% extends "userpage.html" %} +{% extends "userpage/userpage.html" %} {% block content %} @@ -7,7 +7,10 @@