import threading from drama.helpers.wrappers import * from drama.helpers.filters import * from drama.helpers.alerts import * from drama.helpers.aws import * from drama.helpers.session import * from drama.classes import * from drama.routes.front import comment_idlist from pusher_push_notifications import PushNotifications from flask import * from drama.__main__ import app, limiter choices = ['Wow, you must be a JP fan.', 'This is one of the worst posts I have EVER seen. Delete it.', "No, don't reply like this, please do another wall of unhinged rant please.", '# 😴😴😴', "Ma'am we've been over this before. You need to stop.", "I've known more coherent downies.", "Your pulitzer's in the mail", "That's great and all, but I asked for my burger without cheese.", 'That degree finally paying off', "That's nice sweaty. Why don't you have a seat in the time out corner with Pizzashill until you calm down, then you can have your Capri Sun.", "All them words won't bring your pa back.", "You had a chance to not be completely worthless, but it looks like you threw it away. At least you're consistent.", 'Some people are able to display their intelligence by going on at length on a subject and never actually saying anything. This ability is most common in trades such as politics, public relations, and law. You have impressed me by being able to best them all, while still coming off as an absolute idiot.', "You can type 10,000 characters and you decided that these were the one's that you wanted.", 'Have you owned the libs yet?', "I don't know what you said, because I've seen another human naked.", 'Impressive. Normally people with such severe developmental disabilities struggle to write much more than a sentence or two. He really has exceded our expectations for the writing portion. Sadly the coherency of his writing, along with his abilities in the social skills and reading portions, are far behind his peers with similar disabilities.', "This is a really long way of saying you don't fuck.", "Sorry ma'am, looks like his delusions have gotten worse. We'll have to admit him,", 'https://i.kym-cdn.com/photos/images/newsfeed/001/038/094/0a1.jpg', 'If only you could put that energy into your relationships', 'Posts like this is why I do Heroine.', 'still unemployed then?', 'K', 'look im gunna have 2 ask u 2 keep ur giant dumps in the toilet not in my replys 😷😷😷', "Mommy is soooo proud of you, sweaty. Let's put this sperg out up on the fridge with all your other failures.", "Good job bobby, here's a star", "That was a mistake. You're about to find out the hard way why.", 'You sat down and wrote all this shit. You could have done so many other things with your life. What happened to your life that made you decide writing novels of bullshit on reddit was the best option?', "I don't have enough spoons to read this shit", "All those words won't bring daddy back.", 'OUT!', "Mommy is soooo proud of you, sweaty. Let's put this sperg out up on the fridge with all your other failures."] PUSHER_KEY = environ.get("PUSHER_KEY", "").strip() beams_client = PushNotifications( instance_id='02ddcc80-b8db-42be-9022-44c546b4dce6', secret_key=PUSHER_KEY, ) @app.route("/banaward/comment/", methods=["POST"]) @auth_required def banawardcomment(comment_id, v): if v.banawards != 1 and v.banawards != 2: return jsonify({"error": "You must have a ban award to ban this user."}), 403 comment = g.db.query(Comment).filter_by(id=comment_id).first() if not comment: abort(400) u = comment.author if u.admin_level > 0: return jsonify({"error": "You can't ban admins."}), 403 u.ban(admin=v, reason="1 day ban award", days=1) send_notification(1046, u, f"Your Drama account has been suspended for 1 day for the following reason:\n\n> 1 day ban award") ma=ModAction( kind="exile_user", user_id=v.id, target_user_id=u.id, board_id=1, note=f'reason: "1 day ban award", duration: 1 day' ) g.db.add(ma) v.banawards -= 1 g.db.add(v) comment.banaward = v.username g.db.add(comment) return jsonify({"message": "User banned successfully!"}), 200 @app.route("/api/v1/post//comment/", methods=["GET"]) def comment_cid_api_redirect(cid=None, pid=None): redirect(f'/api/v1/comment/') @app.route("/comment/", methods=["GET"]) @app.route("/comment/", methods=["GET"]) @app.route("/post_short//", methods=["GET"]) @app.route("/post_short///", methods=["GET"]) @app.route("/api/v1/comment/", methods=["GET"]) @app.route("/post///", methods=["GET"]) @app.route("/api/vue/comment/") @auth_desired @api("read") def post_pid_comment_cid(cid, pid=None, anything=None, v=None): if v and v.is_banned and not v.unban_utc: return render_template("seized.html") try: cid = int(cid) except Exception as e: pass comment = get_comment(cid, v=v) if not comment.parent_submission and not (v and v.admin_level == 6): abort(403) if not pid: if comment.parent_submission: pid = comment.parent_submission else: pid = 6489 try: pid = int(pid) except: abort(404) post = get_post(pid, v=v) board = post.board if post.over_18 and not (v and v.over_18) and not session_over18(comment.board): t = int(time.time()) return {'html': lambda: render_template("errors/nsfw.html", v=v, t=t, lo_formkey=make_logged_out_formkey( t), board=comment.board ), 'api': lambda: {'error': f'This content is not suitable for some users and situations.'} } post._preloaded_comments = [comment] # context improver try: context = int(request.args.get("context", 0)) except: context = 0 comment_info = comment c = comment while context > 0 and not c.is_top_level: parent = get_comment(c.parent_comment_id, v=v) post._preloaded_comments += [parent] c = parent context -= 1 top_comment = c if v: defaultsortingcomments = v.defaultsortingcomments else: defaultsortingcomments = "top" sort=request.args.get("sort", defaultsortingcomments) # children comments current_ids = [comment.id] exile=g.db.query(ModAction ).filter_by( kind="exile_user" ).distinct(ModAction.target_comment_id).subquery() for i in range(6 - context): if v: votes = g.db.query(CommentVote).filter( CommentVote.user_id == v.id).subquery() blocking = v.blocking.subquery() blocked = v.blocked.subquery() comms = g.db.query( Comment, votes.c.vote_type, blocking.c.id, blocked.c.id, aliased(ModAction, alias=exile) ).select_from(Comment).options( joinedload(Comment.author).joinedload(User.title) ).filter( Comment.parent_comment_id.in_(current_ids) ).join( votes, votes.c.comment_id == Comment.id, isouter=True ).join( blocking, blocking.c.target_id == Comment.author_id, isouter=True ).join( blocked, blocked.c.user_id == Comment.author_id, isouter=True ).join( exile, exile.c.target_comment_id==Comment.id, isouter=True ) if sort == "top": comments = comms.order_by(Comment.score.desc()).all() elif sort == "bottom": comments = comms.order_by(Comment.score.asc()).all() elif sort == "new": comments = comms.order_by(Comment.created_utc.desc()).all() elif sort == "old": comments = comms.order_by(Comment.created_utc.asc()).all() elif sort == "controversial": comments = sorted(comms.all(), key=lambda x: x.score_disputed, reverse=True) elif sort == "random": c = comms.all() comments = random.sample(c, k=len(c)) else: abort(422) output = [] for c in comms: comment = c[0] comment._voted = c[1] or 0 comment._is_blocking = c[2] or 0 comment._is_blocked = c[3] or 0 comment._is_guildmaster=top_comment._is_guildmaster comment._is_exiled_for=c[4] or 0 output.append(comment) else: comms = g.db.query( Comment, aliased(ModAction, alias=exile) ).options( joinedload(Comment.author).joinedload(User.title) ).filter( Comment.parent_comment_id.in_(current_ids) ).join( exile, exile.c.target_comment_id==Comment.id, isouter=True ) if sort == "top": comments = comms.order_by(Comment.score.asc()).all() elif sort == "bottom": comments = comms.order_by(Comment.score.desc()).all() elif sort == "new": comments = comms.order_by(Comment.created_utc.desc()).all() elif sort == "old": comments = comms.order_by(Comment.created_utc.asc()).all() elif sort == "controversial": comments = sorted(comms.all(), key=lambda x: x[0].score_disputed, reverse=True) elif sort == "random": c = comms.all() comments = random.sample(c, k=len(c)) else: abort(422) output = [] for c in comms: comment=c[0] comment._is_exiled_for=c[1] or 0 output.append(comment) post._preloaded_comments += output current_ids = [x.id for x in output] post.tree_comments() post.replies=[top_comment] return {'html': lambda: post.rendered_page(v=v, sort=sort, comment=top_comment, comment_info=comment_info), 'api': lambda: top_comment.json } @app.route("/api/comment", methods=["POST"]) @app.route("/api/v1/comment", methods=["POST"]) @limiter.limit("6/minute") @is_not_banned @no_negative_balance('toast') @tos_agreed @validate_formkey @api("create") def api_comment(v): parent_submission = base36decode(request.form.get("submission")) parent_fullname = request.form.get("parent_fullname") # get parent item info parent_id = parent_fullname.split("_")[1] if parent_fullname.startswith("t2"): parent_post = get_post(parent_id, v=v) parent = parent_post parent_comment_id = None level = 1 parent_submission = base36decode(parent_id) elif parent_fullname.startswith("t3"): parent = get_comment(parent_id, v=v) parent_comment_id = parent.id level = parent.level + 1 parent_id = parent.parent_submission parent_submission = parent_id parent_post = get_post(base36encode(parent_id)) else: abort(400) #process and sanitize body = request.form.get("body", "")[0:10000] body = body.strip() if not body and not request.files.get('file'): return jsonify({"error":"You need to actually write something!"}), 400 for i in re.finditer('^(https:\/\/.*\.(png|jpg|jpeg|gif))', body, re.MULTILINE): body = body.replace(i.group(1), f'![]({i.group(1)})') with CustomRenderer(post_id=parent_id) as renderer: body_md = renderer.render(mistletoe.Document(body)) body_html = sanitize(body_md, linkgen=True) # Run safety filter bans = filter_comment_html(body_html) if bans: ban = bans[0] reason = f"Remove the {ban.domain} link from your comment and try again." if ban.reason: reason += f" {ban.reason_text}" #auto ban for digitally malicious content if any([x.reason==4 for x in bans]): v.ban(days=30, reason="Digitally malicious content") if any([x.reason==7 for x in bans]): v.ban( reason="Sexualizing minors") return jsonify({"error": reason}), 401 # check existing existing = g.db.query(Comment).join(CommentAux).filter(Comment.author_id == v.id, Comment.deleted_utc == 0, Comment.parent_comment_id == parent_comment_id, Comment.parent_submission == parent_submission, CommentAux.body == body ).options(contains_eager(Comment.comment_aux)).first() if existing: return jsonify({"error": f"You already made that comment: {existing.permalink}"}), 409 if parent.author.any_block_exists(v) and not v.admin_level>=3: return jsonify( {"error": "You can't reply to users who have blocked you, or users you have blocked."}), 403 # check for archive and ban state post = get_post(parent_id) if post.is_archived or not post.board.can_comment(v): return jsonify({"error": "You can't comment on this."}), 403 # get bot status is_bot = request.headers.get("X-User-Type","")=="Bot" # check spam - this should hopefully be faster if not is_bot: now = int(time.time()) cutoff = now - 60 * 60 * 24 similar_comments = g.db.query(Comment ).options( lazyload('*') ).join(Comment.comment_aux ).filter( Comment.author_id == v.id, CommentAux.body.op( '<->')(body) < app.config["COMMENT_SPAM_SIMILAR_THRESHOLD"], Comment.created_utc > cutoff ).options(contains_eager(Comment.comment_aux)).all() threshold = app.config["COMMENT_SPAM_COUNT_THRESHOLD"] if v.age >= (60 * 60 * 24 * 7): threshold *= 3 elif v.age >= (60 * 60 * 24): threshold *= 2 if len(similar_comments) > threshold: text = "Your Drama account has been suspended for 1 day for the following reason:\n\n> Too much spam!" send_notification(1046, v, text) v.ban(reason="Spamming.", days=1) for alt in v.alts: if not alt.is_suspended: alt.ban(reason="Spamming.") for comment in similar_comments: comment.is_banned = True comment.ban_reason = "Automatic spam removal. This happened because the post's creator submitted too much similar content too quickly." g.db.add(comment) ma=ModAction( user_id=2317, target_comment_id=comment.id, kind="ban_comment", board_id=comment.post.board_id, note="spam" ) g.db.add(ma) g.db.commit() return jsonify({"error": "Too much spam!"}), 403 # check badlinks soup = BeautifulSoup(body_html, features="html.parser") links = [x['href'] for x in soup.find_all('a') if x.get('href')] for link in links: parse_link = urlparse(link) check_url = ParseResult(scheme="https", netloc=parse_link.netloc, path=parse_link.path, params=parse_link.params, query=parse_link.query, fragment='') check_url = urlunparse(check_url) badlink = g.db.query(BadLink).filter( literal(check_url).contains( BadLink.link)).first() if badlink: return jsonify({"error": f"Remove the following link and try again: `{check_url}`. Reason: {badlink.reason_text}"}), 403 if v.shadowbanned: shadowbanned = True else: shadowbanned = False # create comment c = Comment(author_id=v.id, parent_submission=parent_submission, parent_fullname=parent.fullname, parent_comment_id=parent_comment_id, level=level, over_18=post.over_18 or request.form.get("over_18","")=="true", is_nsfl=post.is_nsfl or request.form.get("is_nsfl","")=="true", original_board_id=parent_post.board_id, is_bot=is_bot, app_id=v.client.application.id if v.client else None, shadowbanned=shadowbanned ) g.db.add(c) g.db.flush() if v.dramacoins >= 0: if request.files.get("file"): file=request.files["file"] if not file.content_type.startswith('image/'): return jsonify({"error": "That wasn't an image!"}), 400 name = f'comment/{c.base36id}/{secrets.token_urlsafe(8)}' url = upload_file(name, file) body = request.form.get("body") + f"\n\n![]({url})" with CustomRenderer(post_id=parent_id) as renderer: body_md = renderer.render(mistletoe.Document(body)) body_html = sanitize(body_md, linkgen=True) # #csam detection # def del_function(): # delete_file(name) # c.is_banned=True # g.db.add(c) # g.db.commit() # csam_thread=threading.Thread(target=check_csam_url, # args=(f"https://s3.eu-central-1.amazonaws.com/i.drama.ga/{name}", # v, # del_function # ) # ) # csam_thread.start() c_aux = CommentAux( id=c.id, body_html=body_html, body=body ) g.db.add(c_aux) g.db.flush() if v.agendaposter and "trans lives matter" not in c_aux.body_html.lower(): c.is_banned = True c.ban_reason = "ToS Violation" g.db.add(c) c_jannied = Comment(author_id=2317, parent_submission=parent_submission, distinguish_level=6, parent_fullname=c.fullname, parent_comment_id=c.id, level=level+1, original_board_id=1, is_bot=True, ) g.db.add(c_jannied) g.db.flush() body = f"""Hi @{v.username},\n\nYour comment has been automatically removed because you forgot to include `trans lives matter`.\n\nDon't worry, we're here to help! We won't let you post or comment anything that doesn't express your love and acceptance towards the trans community. Feel free to resubmit your comment with `trans lives matter` included. \n\n*This is an automated message; if you need help, you can message us [here](/contact).*""" with CustomRenderer(post_id=parent_id) as renderer: body_md = renderer.render(mistletoe.Document(body)) body_jannied_html = sanitize(body_md) c_aux = CommentAux( id=c_jannied.id, body_html=body_jannied_html, body=body ) g.db.add(c_aux) g.db.flush() n = Notification(comment_id=c_jannied.id, user_id=v.id) g.db.add(n) if len(body) >= 1000 and v.username != "Snappy" and "" not in body_html: c2 = Comment(author_id=1832, parent_submission=parent_submission, parent_fullname=c.fullname, parent_comment_id=c.id, level=level+1, original_board_id=1, is_bot=True, ) g.db.add(c2) g.db.flush() body = random.choice(choices) with CustomRenderer(post_id=parent_id) as renderer: body_md = renderer.render(mistletoe.Document(body)) body_html2 = sanitize(body_md, linkgen=True) c_aux = CommentAux( id=c2.id, body_html=body_html2, body=body ) g.db.add(c_aux) g.db.flush() n = Notification(comment_id=c2.id, user_id=v.id) g.db.add(n) if random.random() < 0.001 and v.username != "Snappy" and v.username != "zozbot": c2 = Comment(author_id=1833, parent_submission=parent_submission, parent_fullname=c.fullname, parent_comment_id=c.id, level=level+1, original_board_id=1, is_bot=True, ) g.db.add(c2) g.db.flush() body = "zoz" with CustomRenderer(post_id=parent_id) as renderer: body_md = renderer.render(mistletoe.Document(body)) body_html2 = sanitize(body_md, linkgen=True) c_aux = CommentAux( id=c2.id, body_html=body_html2, body=body ) g.db.add(c_aux) g.db.flush() n = Notification(comment_id=c2.id, user_id=v.id) g.db.add(n) c3 = Comment(author_id=1833, parent_submission=parent_submission, parent_fullname=c2.fullname, parent_comment_id=c2.id, level=level+2, original_board_id=1, is_bot=True, ) g.db.add(c3) g.db.flush() body = "zle" with CustomRenderer(post_id=parent_id) as renderer: body_md = renderer.render(mistletoe.Document(body)) body_html2 = sanitize(body_md, linkgen=True) c_aux = CommentAux( id=c3.id, body_html=body_html2, body=body ) g.db.add(c_aux) g.db.flush() c4 = Comment(author_id=1833, parent_submission=parent_submission, parent_fullname=c3.fullname, parent_comment_id=c3.id, level=level+3, original_board_id=1, is_bot=True, ) g.db.add(c4) g.db.flush() body = "zozzle" with CustomRenderer(post_id=parent_id) as renderer: body_md = renderer.render(mistletoe.Document(body)) body_html2 = sanitize(body_md, linkgen=True) c_aux = CommentAux( id=c4.id, body_html=body_html2, body=body ) g.db.add(c_aux) g.db.flush() if not shadowbanned: # queue up notification for parent author notify_users = set() for x in g.db.query(Subscription.user_id).filter_by(submission_id=c.parent_submission).all(): notify_users.add(x) if parent.author.id != v.id: notify_users.add(parent.author.id) soup = BeautifulSoup(body_html, features="html.parser") mentions = soup.find_all("a", href=re.compile("^/@(\w+)")) for mention in mentions: username = mention["href"].split("@")[1] user = g.db.query(User).filter_by(username=username).first() if user: if v.any_block_exists(user): continue if user.id != v.id: notify_users.add(user.id) for x in notify_users: n = Notification(comment_id=c.id, user_id=x) g.db.add(n) try: g.db.flush() except: g.db.rollback() if parent.author.id != v.id: beams_client.publish_to_interests( interests=[str(parent.author.id)], publish_body={ 'web': { 'notification': { 'title': f'New reply by @{v.username}', 'body': c.body, 'deep_link': f'https://rdrama.net{c.permalink}?context=5#context', }, }, }, ) # create auto upvote vote = CommentVote(user_id=v.id, comment_id=c.id, vote_type=1 ) g.db.add(vote) c=get_comment(c.id, v=v) # print(f"Content Event: @{v.username} comment {c.base36id}") board = get_board(1) cache.delete_memoized(comment_idlist) cache.delete_memoized(User.commentlisting, v) return {"html": lambda: jsonify({"html": render_template("comments.html", v=v, comments=[c], render_replies=False, is_allowed_to_comment=True )}), "api": lambda: c.json } @app.route("/edit_comment/", methods=["POST"]) @is_not_banned @validate_formkey @api("edit") def edit_comment(cid, v): c = get_comment(cid, v=v) if not c.author_id == v.id: abort(403) if c.is_banned or c.deleted_utc > 0: abort(403) body = request.form.get("body", "")[0:10000] for i in re.finditer('^(https:\/\/.*\.(png|jpg|jpeg|gif))', body, re.MULTILINE): body = body.replace(i.group(1), f'![]({i.group(1)})') with CustomRenderer(post_id=c.post.base36id) as renderer: body_md = renderer.render(mistletoe.Document(body)) body_html = sanitize(body_md, linkgen=True) bans = filter_comment_html(body_html) if bans: ban = bans[0] reason = f"Remove the {ban.domain} link from your comment and try again." #auto ban for digitally malicious content if any([x.reason==4 for x in bans]): v.ban(days=30, reason="Digitally malicious content is not allowed.") return jsonify({"error":"Digitally malicious content is not allowed."}) if ban.reason: reason += f" {ban.reason_text}" return jsonify({"error": reason}), 401 return {'html': lambda: render_template("comment_failed.html", action=f"/edit_comment/{c.base36id}", badlinks=[ x.domain for x in bans], body=body, v=v ), 'api': lambda: ({'error': f'A blacklisted domain was used.'}, 400) } # check badlinks soup = BeautifulSoup(body_html, features="html.parser") links = [x['href'] for x in soup.find_all('a') if x.get('href')] for link in links: parse_link = urlparse(link) check_url = ParseResult(scheme="https", netloc=parse_link.netloc, path=parse_link.path, params=parse_link.params, query=parse_link.query, fragment='') check_url = urlunparse(check_url) badlink = g.db.query(BadLink).filter( literal(check_url).contains( BadLink.link)).first() if badlink: return jsonify({"error": f"Remove the following link and try again: `{check_url}`. Reason: {badlink.reason_text}"}), 403 # check spam - this should hopefully be faster now = int(time.time()) cutoff = now - 60 * 60 * 24 similar_comments = g.db.query(Comment ).options( lazyload('*') ).join(Comment.comment_aux ).filter( Comment.author_id == v.id, CommentAux.body.op( '<->')(body) < app.config["SPAM_SIMILARITY_THRESHOLD"], Comment.created_utc > cutoff ).options(contains_eager(Comment.comment_aux)).all() threshold = app.config["SPAM_SIMILAR_COUNT_THRESHOLD"] if v.age >= (60 * 60 * 24 * 30): threshold *= 4 elif v.age >= (60 * 60 * 24 * 7): threshold *= 3 elif v.age >= (60 * 60 * 24): threshold *= 2 if len(similar_comments) > threshold: text = "Your Drama account has been suspended for 1 day for the following reason:\n\n> Too much spam!" send_notification(1046, v, text) v.ban(reason="Spamming.", days=1) for comment in similar_comments: comment.is_banned = True comment.ban_reason = "Automatic spam removal. This happened because the post's creator submitted too much similar content too quickly." g.db.add(comment) g.db.commit() return jsonify({"error": "Too much spam!"}), 403 if v.dramacoins >= 0: if request.files.get("file"): file=request.files["file"] if not file.content_type.startswith('image/'): return jsonify({"error": "That wasn't an image!"}), 400 name = f'comment/{c.base36id}/{secrets.token_urlsafe(8)}' url = upload_file(name, file) body += f"\n\n![]({url})" with CustomRenderer(post_id=c.parent_submission) as renderer: body_md = renderer.render(mistletoe.Document(body)) body_html = sanitize(body_md, linkgen=True) # #csam detection # def del_function(): # delete_file(name) # c.is_banned=True # g.db.add(c) # g.db.commit() # csam_thread=threading.Thread(target=check_csam_url, # args=(f"https://s3.eu-central-1.amazonaws.com/i.drama.ga/{name}", # v, # del_function # ) # ) # csam_thread.start() c.body = body c.body_html = body_html if v.agendaposter and "trans lives matter" not in c.body_html.lower(): c.is_banned = True c.ban_reason = "ToS Violation" g.db.add(c) c_jannied = Comment(author_id=2317, parent_submission=c.parent_submission, distinguish_level=6, parent_fullname=c.fullname, parent_comment_id=c.id, level=c.level+1, original_board_id=1, is_bot=True, ) g.db.add(c_jannied) g.db.flush() body = f"""Hi @{v.username},\n\nYour comment has been automatically removed because you forgot to include `trans lives matter`.\n\nDon't worry, we're here to help! We won't let you post or comment anything that doesn't express your love and acceptance towards the trans community. Feel free to resubmit your comment with `trans lives matter` included. \n\n*This is an automated message; if you need help, you can message us [here](/contact).*""" with CustomRenderer(post_id=c.parent_submission) as renderer: body_md = renderer.render(mistletoe.Document(body)) body_jannied_html = sanitize(body_md) c_aux = CommentAux( id=c_jannied.id, body_html=body_jannied_html, body=body ) g.db.add(c_aux) g.db.flush() n = Notification(comment_id=c_jannied.id, user_id=v.id) g.db.add(n) if int(time.time()) - c.created_utc > 60 * 3: c.edited_utc = int(time.time()) g.db.add(c) g.db.commit() path = request.form.get("current_page", "/") # queue up notifications for username mentions notify_users = set() soup = BeautifulSoup(body_html, features="html.parser") mentions = soup.find_all("a", href=re.compile("^/@(\w+)")) if len(mentions) > 0: notifs = g.db.query(Notification) for mention in mentions: username = mention["href"].split("@")[1] user = g.db.query(User).filter_by(username=username).first() if user: if v.any_block_exists(user): continue if user.id != v.id: notify_users.add(user.id) for x in notify_users: notif = notifs.filter_by(comment_id=c.id, user_id=x).first() if not notif: n = Notification(comment_id=c.id, user_id=x) g.db.add(n) return jsonify({"html": c.body_html}) @app.route("/delete/comment/", methods=["POST"]) @app.route("/api/v1/delete/comment/", methods=["POST"]) @auth_required @validate_formkey @api("delete") def delete_comment(cid, v): c = g.db.query(Comment).filter_by(id=base36decode(cid)).first() if not c: abort(404) if not c.author_id == v.id: abort(403) c.deleted_utc = int(time.time()) g.db.add(c) cache.delete_memoized(User.commentlisting, v) return {"html": lambda: ("", 204), "api": lambda: ("", 204)} @app.route("/undelete/comment/", methods=["POST"]) @app.route("/api/v1/undelete/comment/", methods=["POST"]) @auth_required @validate_formkey @api("delete") def undelete_comment(cid, v): c = g.db.query(Comment).filter_by(id=base36decode(cid)).first() if not c: abort(404) if not c.author_id == v.id: abort(403) c.deleted_utc = 0 g.db.add(c) cache.delete_memoized(User.commentlisting, v) return {"html": lambda: ("", 204), "api": lambda: ("", 204)} @app.route("/embed/comment/", methods=["GET"]) @app.route("/embed/post//comment/", methods=["GET"]) @app.route("/api/v1/embed/comment/", methods=["GET"]) @app.route("/api/v1/embed/post//comment/", methods=["GET"]) def embed_comment_cid(cid, pid=None): comment = get_comment(cid) if not comment.parent: abort(403) if comment.is_banned or comment.deleted_utc > 0: return {'html': lambda: render_template("embeds/comment_removed.html", c=comment), 'api': lambda: {'error': f'Comment {cid} has been removed'} } if comment.board.is_banned: abort(410) return render_template("embeds/comment.html", c=comment) @app.route("/comment_pin/", methods=["POST"]) @auth_required @validate_formkey def mod_toggle_comment_pin(cid, v): comment = get_comment(cid, v=v) if v.admin_level != 6 and v.id != comment.post.author_id: abort(403) comment.is_pinned = not comment.is_pinned g.db.add(comment) g.db.commit() if v.admin_level == 6: ma=ModAction( kind="pin_comment" if comment.is_pinned else "unpin_comment", user_id=v.id, board_id=1, target_comment_id=comment.id ) g.db.add(ma) html=render_template( "comments.html", v=v, comments=[comment], render_replies=False, is_allowed_to_comment=True ) html=str(BeautifulSoup(html, features="html.parser").find(id=f"comment-{comment.base36id}-only")) return jsonify({"html":html}) @app.route("/save_comment/", methods=["POST"]) @auth_required @validate_formkey def save_comment(cid, v): comment=get_comment(cid) new_save=SaveRelationship(user_id=v.id, submission_id=comment.id, type=2) g.db.add(new_save) try: g.db.flush() except: abort(422) return "", 204 @app.route("/unsave_comment/", methods=["POST"]) @auth_required @validate_formkey def unsave_comment(cid, v): comment=get_comment(cid) save=g.db.query(SaveRelationship).filter_by(user_id=v.id, submission_id=comment.id, type=2).first() g.db.delete(save) return "", 204