def submit_post(v): try: title=request.form.get("title","") title=title.lstrip().rstrip() title=title.replace("\n","") title=title.replace("\r","") title=title.replace("\t","") url=request.form.get("url","") board=get_guild(request.form.get('board','general'), graceful=True) if not board: board=get_guild('general') if not title: raise Exception("Please enter a better title.") elif len(title)>500: raise Exception("500 character limit for titles.") parsed_url=urlparse(url) if not (parsed_url.scheme and parsed_url.netloc) and not request.form.get("body") and not request.files.get("file",None): raise Exception("Please enter a URL or some text.") #sanitize title title=bleach.clean(title) #Force https for submitted urls if request.form.get("url"): new_url=ParseResult(scheme="https", netloc=parsed_url.netloc, path=parsed_url.path, params=parsed_url.params, query=parsed_url.query, fragment=parsed_url.fragment) url=urlunparse(new_url) else: url="" body=request.form.get("body","") #check for duplicate dup = g.db.query(Submission).join(Submission.submission_aux).filter( Submission.author_id==v.id, Submission.is_deleted==False, Submission.board_id==board.id, SubmissionAux.title==title, SubmissionAux.url==url, SubmissionAux.body==body ).first() if dup: return redirect(dup.permalink) #check for domain specific rules parsed_url=urlparse(url) domain=parsed_url.netloc # check ban status domain_obj=get_domain(domain) if domain_obj: if not domain_obj.can_submit: raise Exception(error=BAN_REASONS[domain_obj.reason]) #check for embeds if domain_obj.embed_function: try: embed=eval(domain_obj.embed_function)(url) except: embed="" else: embed="" else: embed="" #board board_name=request.form.get("board","general") board_name=board_name.lstrip("+") board_name=board_name.rstrip() board=get_guild(board_name, graceful=True) if not board: board=get_guild('general') if board.is_banned: raise Exception(f"+{board.name} has been banned.::403 Forbidden - +{board.name} has been banned") if board.has_ban(v): raise Exception(f"You are exiled from +{board.name}.::403 Not Authorized - You are exiled from +{board.name}") if (board.restricted_posting or board.is_private) and not (board.can_submit(v)): raise Exception(f"You are not an approved contributor for +{board.name}.::403 Not Authorized - You are not an approved contributor for +{board.name}") #similarity check now=int(time.time()) cutoff=now-60*60*24 similar_posts = g.db.query(Submission).options( lazyload('*') ).join(Submission.submission_aux ).filter( Submission.author_id==v.id, SubmissionAux.title.op('<->')(title)<app.config["SPAM_SIMILARITY_THRESHOLD"], Submission.created_utc>cutoff ).all() if url: similar_urls=g.db.query(Submission).options( lazyload('*') ).join(Submission.submission_aux ).filter( Submission.author_id==v.id, SubmissionAux.url.op('<->')(url)<app.config["SPAM_URL_SIMILARITY_THRESHOLD"], Submission.created_utc>cutoff ).all() else: similar_urls=[] 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 max(len(similar_urls), len(similar_posts)) >= threshold: text="Your Ruqqus account has been suspended for 1 day for the following reason:\n\n> Too much spam!" send_notification(v, text) v.ban(reason="Spamming.", include_alts=True, days=1) for post in similar_posts+similar_urls: post.is_banned=True post.ban_reason="Automatic spam removal. This happened because the post's creator submitted too much similar content too quickly." g.db.add(post) g.db.commit() return redirect("/notifications") # Catch too-long body if len(str(body))>10000: raise Exception("10000 character limit for text body") if len(url)>2048: raise Exception("URLs cannot be over 2048 characters") # Render text with CustomRenderer() as renderer: body_md=renderer.render(mistletoe.Document(body)) body_html = sanitize(body_md, linkgen=True) # Check spam soup=BeautifulSoup(body_html, features="html.parser") links=[x['href'] for x in soup.find_all('a') if x.get('href')] if url: links=[url]+links 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: if badlink.autoban: text="Your Ruqqus account has been suspended for 1 day for the following reason:\n\n> Too much spam!" send_notification(v, text) v.ban(days=1, reason="spam") return redirect('/notifications') else: raise Exception(f"The link `{badlink.link}` is not allowed. Reason: {badlink.reason}") # Check for embeddable video domain=parsed_url.netloc if url: repost = g.db.query(Submission).join(Submission.submission_aux).filter( SubmissionAux.url.ilike(url), Submission.board_id==board.id, Submission.is_deleted==False, Submission.is_banned==False ).order_by( Submission.id.asc() ).first() else: repost=None if request.files.get('file') and not v.can_submit_image: raise Exception(f"You cannot submit images") # Offensive is_offensive=False for x in g.db.query(BadWord).all(): if (body and x.check(body)) or x.check(title): is_offensive=True break new_post=Submission(author_id=v.id, domain_ref=domain_obj.id if domain_obj else None, board_id=board.id, original_board_id=board.id, over_18=(bool(request.form.get("over_18","")) or board.over_18), post_public=not board.is_private, repost_id=repost.id if repost else None, is_offensive=is_offensive ) g.db.add(new_post) g.db.flush() new_post_aux=SubmissionAux(id=new_post.id, url=url, body=body, body_html=body_html, embed_url=embed, title=title ) g.db.add(new_post_aux) g.db.flush() vote=Vote(user_id=v.id, vote_type=1, submission_id=new_post.id ) g.db.add(vote) g.db.flush() g.db.commit() g.db.refresh(new_post) # Check for uploaded image if request.files.get('file'): file=request.files['file'] name=f'post/{new_post.base36id}/{secrets.token_urlsafe(8)}' upload_file(name, file) #thumb_name=f'posts/{new_post.base36id}/thumb.png' #upload_file(name, file, resize=(375,227)) # Update post data new_post.url=f'https://{BUCKET}/{name}' new_post.is_image=True new_post.domain_ref=1 #id of i.ruqqus.com domain g.db.add(new_post) g.db.commit() # Spin off thumbnail generation and csam detection as new threads if new_post.url or request.files.get('file'): new_thread=threading.Thread(target=thumbnail_thread, args=(new_post.base36id,) ) new_thread.start() csam_thread = threading.Thread(target=check_csam, args=(new_post,)) csam_thread.start() # Expire the relevant caches: front page new, board new # cache.delete_memoized(frontlist, sort="new") g.db.commit() cache.delete_memoized(Board.idlist, board, sort="new") return {"html":lambda:redirect(new_post.permalink), "api":lambda:jsonify(new_post.json)} except Exception as e: string = e.split("::") # Return a mixed (API+HTML) response if len(string) > 1: # Do a lambda html_string = string[0] api_string = string[1] # Render the mixed html+API return {"html":lambda:(render_template("submit.html", v=v, error=html_string, title=title, url=url, body=request.form.get("body",""), b=get_guild(request.form.get("board","general"), graceful=True ) ),403), "api":lambda:(jsonify({"error":api_string}), 403)} # Return a normal template (no :: separator) else: return render_template("submit.html", v=v, error=e, title=title, text=body[0:2000], b=get_guild(request.form.get("board","general"), graceful=True) ), 400
def api_comment(v): parent_submission=base36decode(request.form.get("submission")) parent_fullname=request.form.get("parent_fullname") #process and sanitize body=request.form.get("body","")[0:10000] body=body.lstrip().rstrip() with CustomRenderer(post_id=request.form.get("submission")) 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: return render_template("comment_failed.html", action="/api/comment", parent_submission=request.form.get("submission"), parent_fullname=request.form.get("parent_fullname"), badlinks=[x.domain for x in bans], body=body, is_deleted=False, v=v ), 422 #check existing existing=g.db.query(Comment).filter_by(author_id=v.id, body=body, is_deleted=False, parent_fullname=parent_fullname, parent_submission=parent_submission ).first() if existing: return jsonify({"error":"You already made that comment."}), 409 #get parent item info parent_id=int(parent_fullname.split("_")[1], 36) if parent_fullname.startswith("t2"): parent=get_post(parent_id, v=v) parent_comment_id=None level=1 elif parent_fullname.startswith("t3"): parent=get_comment(parent_id, v=v) parent_comment_id=parent.id level=parent.level+1 #No commenting on deleted/removed things if parent.is_banned or parent.is_deleted: return jsonify({"error":"You can't comment on things that have been deleted."}), 403 if parent.author.any_block_exists(v): 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(request.form.get("submission")) if post.is_archived or not post.board.can_comment(v): return jsonify({"error":"You can't comment on this."}), 403 #create comment c=Comment(author_id=v.id, body=body, body_html=body_html, parent_submission=parent_submission, parent_fullname=parent_fullname, parent_comment_id=parent_comment_id, level=level, author_name=v.username, over_18=post.over_18, is_nsfl=post.is_nsfl, is_op=(v.id==post.author_id) ) c.determine_offensive() g.db.add(c) g.db.commit() notify_users=set() #queue up notification for parent author if parent.author.id != v.id: notify_users.add(parent.author.id) #queue up notifications for username mentions soup=BeautifulSoup(c.body_html, features="html.parser") mentions=soup.find_all("a", href=re.compile("^/@(\w+)"), limit=3) 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 notify_users.add(user.id) for x in notify_users: n=Notification(comment_id=c.id, user_id=x) g.db.add(n) #create auto upvote vote=CommentVote(user_id=v.id, comment_id=c.id, vote_type=1 ) g.db.add(vote) #print(f"Content Event: @{v.username} comment {c.base36id}") return jsonify({"html":render_template("comments.html", v=v, comments=[c], render_replies=False, is_allowed_to_comment=True ) } )
def api_comment(v): parent_submission=base36decode(request.form.get("submission")) parent_fullname=request.form.get("parent_fullname") #process and sanitize body=request.form.get("body","")[0:10000] body=body.lstrip().rstrip() with CustomRenderer(post_id=request.form.get("submission")) 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}" return jsonify({"error": reason}), 401 #get parent item info parent_id=parent_fullname.split("_")[1] if parent_fullname.startswith("t2"): parent=get_post(parent_id, v=v) parent_comment_id=None level=1 elif parent_fullname.startswith("t3"): parent=get_comment(parent_id, v=v) parent_comment_id=parent.id level=parent.level+1 #check existing existing=g.db.query(Comment).join(CommentAux).filter(Comment.author_id==v.id, Comment.is_deleted==False, Comment.parent_comment_id==parent_comment_id, Comment.parent_submission==parent_submission, CommentAux.body==body ).first() if existing: return jsonify({"error":f"You already made that comment: {existing.permalink}"}), 409 #No commenting on deleted/removed things if parent.is_banned or parent.is_deleted: return jsonify({"error":"You can't comment on things that have been deleted."}), 403 if parent.author.any_block_exists(v): 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(request.form.get("submission")) if post.is_archived or not post.board.can_comment(v): return jsonify({"error":"You can't comment on this."}), 403 for x in g.db.query(BadWord).all(): if x.check(body): is_offensive=True break else: is_offensive=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, is_nsfl=post.is_nsfl, is_op=(v.id==post.author_id), is_offensive=is_offensive ) g.db.add(c) g.db.flush() c_aux=CommentAux( id=c.id, body_html=body_html, body=body ) g.db.add(c_aux) g.db.flush() notify_users=set() #queue up notification for parent author if parent.author.id != v.id: notify_users.add(parent.author.id) #queue up notifications for username mentions soup=BeautifulSoup(body_html, features="html.parser") mentions=soup.find_all("a", href=re.compile("^/@(\w+)"), limit=3) 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 notify_users.add(user.id) for x in notify_users: n=Notification(comment_id=c.id, user_id=x) g.db.add(n) #create auto upvote vote=CommentVote(user_id=v.id, comment_id=c.id, vote_type=1 ) g.db.add(vote) g.db.commit() #print(f"Content Event: @{v.username} comment {c.base36id}") return jsonify({"html":render_template("comments.html", v=v, comments=[c], render_replies=False, is_allowed_to_comment=True ) } )
def __init__(self, text: str): self.doc = mistletoe.Document(text)
def settings_profile_post(v): updated = False if request.form.get("new_password"): if request.form.get("new_password") != request.form.get( "cnf_password"): return render_template("settings.html", v=v, error="Passwords do not match.") if not v.verifyPass(request.form.get("old_password")): return render_template("settings.html", v=v, error="Incorrect password") v.passhash = v.hash_password(request.form.get("new_password")) updated = True if request.form.get("over18") != v.over_18: updated = True v.over_18 = bool(request.form.get("over18", None)) cache.delete_memoized(User.idlist, v) if request.form.get("hide_offensive") != v.hide_offensive: updated = True v.hide_offensive = bool(request.form.get("hide_offensive", None)) cache.delete_memoized(User.idlist, v) if request.form.get("show_nsfl") != v.show_nsfl: updated = True v.show_nsfl = bool(request.form.get("show_nsfl", None)) cache.delete_memoized(User.idlist, v) if request.form.get("private") != v.is_private: updated = True v.is_private = bool(request.form.get("private", None)) if request.form.get("bio") != v.bio: updated = True bio = request.form.get("bio")[0:256] v.bio = bio with CustomRenderer() as renderer: v.bio_html = renderer.render(mistletoe.Document(bio)) v.bio_html = sanitize(v.bio_html, linkgen=True) x = int(request.form.get("title_id", 0)) if x == 0: v.title_id = None updated = True elif x > 0: title = get_title(x) if bool(eval(title.qualification_expr)): v.title_id = title.id updated = True else: return render_template( "settings_profile.html", v=v, error= f"Unable to set title {title.text} - {title.requirement_string}" ) else: abort(400) if updated: g.db.add(v) return render_template("settings_profile.html", v=v, msg="Your settings have been saved.") else: return render_template("settings_profile.html", v=v, error="You didn't change anything.")
def submit_post(v): title = request.form.get("title", "").lstrip().rstrip() title = title.lstrip().rstrip() title = title.replace("\n", "") title = title.replace("\r", "") title = title.replace("\t", "") url = request.form.get("url", "") board = get_guild(request.form.get('board', 'general'), graceful=True) if not board: board = get_guild('general') if not title: return { "html": lambda: (render_template("submit.html", v=v, error="Please enter a better title.", title=title, url=url, body=request.form.get("body", ""), b=board), 400), "api": lambda: ({ "error": "Please enter a better title" }, 400) } # if len(title)<10: # return render_template("submit.html", # v=v, # error="Please enter a better title.", # title=title, # url=url, # body=request.form.get("body",""), # b=board # ) elif len(title) > 500: return { "html": lambda: (render_template("submit.html", v=v, error="500 character limit for titles.", title=title[0:500], url=url, body=request.form.get("body", ""), b=board), 400), "api": lambda: ({ "error": "500 character limit for titles" }, 400) } parsed_url = urlparse(url) if not (parsed_url.scheme and parsed_url.netloc) and not request.form.get( "body") and not request.files.get("file", None): return { "html": lambda: (render_template("submit.html", v=v, error="Please enter a url or some text.", title=title, url=url, body=request.form.get("body", ""), b=board), 400), "api": lambda: ({ "error": "`url` or `body` parameter required." }, 400) } # sanitize title title = bleach.clean(title, tags=[]) # Force https for submitted urls if request.form.get("url"): new_url = ParseResult(scheme="https", netloc=parsed_url.netloc, path=parsed_url.path, params=parsed_url.params, query=parsed_url.query, fragment=parsed_url.fragment) url = urlunparse(new_url) else: url = "" body = request.form.get("body", "") # check for duplicate dup = g.db.query(Submission).join(Submission.submission_aux).filter( Submission.author_id == v.id, Submission.deleted_utc == 0, Submission.board_id == board.id, SubmissionAux.title == title, SubmissionAux.url == url, SubmissionAux.body == body).first() if dup: return redirect(dup.permalink) # check for domain specific rules parsed_url = urlparse(url) domain = parsed_url.netloc # check ban status domain_obj = get_domain(domain) if domain_obj: if not domain_obj.can_submit: if domain_obj.reason == 4: v.ban(days=30, reason="Digitally malicious content") elif domain_obj.reason == 7: v.ban(reason="Sexualizing minors") return { "html": lambda: (render_template("submit.html", v=v, error=BAN_REASONS[domain_obj.reason], title=title, url=url, body=request.form.get("body", ""), b=board), 400), "api": lambda: ({ "error": BAN_REASONS[domain_obj.reason] }, 400) } # check for embeds if domain_obj.embed_function: try: embed = eval(domain_obj.embed_function)(url) except BaseException: embed = "" else: embed = "" else: embed = "" # board board_name = request.form.get("board", "general") board_name = board_name.lstrip("+") board_name = board_name.rstrip() board = get_guild(board_name, graceful=True) if not board: board = get_guild('general') if board.is_banned: return { "html": lambda: (render_template("submit.html", v=v, error=f"+{board.name} has been banned.", title=title, url=url, body=request.form.get("body", ""), b=get_guild("general", graceful=True)), 403), "api": lambda: (jsonify( {"error": f"403 Forbidden - +{board.name} has been banned."})) } if board.has_ban(v): return { "html": lambda: (render_template("submit.html", v=v, error=f"You are exiled from +{board.name}.", title=title, url=url, body=request.form.get("body", ""), b=get_guild("general")), 403), "api": lambda: (jsonify({ "error": f"403 Not Authorized - You are exiled from +{board.name}" }), 403) } if (board.restricted_posting or board.is_private) and not (board.can_submit(v)): return { "html": lambda: (render_template( "submit.html", v=v, error= f"You are not an approved contributor for +{board.name}.", title=title, url=url, body=request.form.get("body", ""), b=get_guild(request.form.get("board", "general"), graceful=True)), 403), "api": lambda: (jsonify({ "error": f"403 Not Authorized - You are not an approved contributor for +{board.name}" }), 403) } # similarity check now = int(time.time()) cutoff = now - 60 * 60 * 24 similar_posts = g.db.query(Submission).options(lazyload('*')).join( Submission.submission_aux ).filter( #or_( # and_( Submission.author_id == v.id, SubmissionAux.title.op('<->')(title) < app.config["SPAM_SIMILARITY_THRESHOLD"], Submission.created_utc > cutoff # ), # and_( # SubmissionAux.title.op('<->')(title) < app.config["SPAM_SIMILARITY_THRESHOLD"]/2, # Submission.created_utc > cutoff # ) #) ).all() if url: similar_urls = g.db.query(Submission).options(lazyload('*')).join( Submission.submission_aux ).filter( #or_( # and_( Submission.author_id == v.id, SubmissionAux.url.op('<->')(url) < app.config["SPAM_URL_SIMILARITY_THRESHOLD"], Submission.created_utc > cutoff # ), # and_( # SubmissionAux.url.op('<->')(url) < app.config["SPAM_URL_SIMILARITY_THRESHOLD"]/2, # Submission.created_utc > cutoff # ) #) ).all() else: similar_urls = [] threshold = app.config["SPAM_SIMILAR_COUNT_THRESHOLD"] if v.age >= (60 * 60 * 24 * 7): threshold *= 3 elif v.age >= (60 * 60 * 24): threshold *= 2 if max(len(similar_urls), len(similar_posts)) >= threshold: text = "Your Ruqqus account has been suspended for 1 day for the following reason:\n\n> Too much spam!" send_notification(v, text) v.ban(reason="Spamming.", days=1) for alt in v.alts: if not alt.is_suspended: alt.ban(reason="Spamming.", days=1) for post in similar_posts + similar_urls: post.is_banned = True post.is_pinned = False post.ban_reason = "Automatic spam removal. This happened because the post's creator submitted too much similar content too quickly." g.db.add(post) ma = ModAction(user_id=1, target_submission_id=post.id, kind="ban_post", board_id=post.board_id, note="spam") g.db.add(ma) g.db.commit() return redirect("/notifications") # catch too-long body if len(str(body)) > 10000: return { "html": lambda: (render_template("submit.html", v=v, error="10000 character limit for text body.", title=title, url=url, body=request.form.get("body", ""), b=board), 400), "api": lambda: ({ "error": "10000 character limit for text body." }, 400) } if len(url) > 2048: return { "html": lambda: (render_template("submit.html", v=v, error="2048 character limit for URLs.", title=title, url=url, body=request.form.get("body", ""), b=board), 400), "api": lambda: ({ "error": "2048 character limit for URLs." }, 400) } # render text body = preprocess(body) with CustomRenderer() 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 post 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 is not allowed.") abort(403) return { "html": lambda: (render_template("submit.html", v=v, error=reason, title=title, url=url, body=request.form.get("body", ""), b=board), 403), "api": lambda: ({ "error": reason }, 403) } # check spam soup = BeautifulSoup(body_html, features="html.parser") links = [x['href'] for x in soup.find_all('a') if x.get('href')] if url: links = [url] + links 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: if badlink.autoban: text = "Your Ruqqus account has been suspended for 1 day for the following reason:\n\n> Too much spam!" send_notification(v, text) v.ban(days=1, reason="spam") return redirect('/notifications') else: return { "html": lambda: (render_template( "submit.html", v=v, error= f"The link `{badlink.link}` is not allowed. Reason: {badlink.reason}.", title=title, url=url, body=request.form.get("body", ""), b=board), 400), "api": lambda: ({ "error": f"The link `{badlink.link}` is not allowed. Reason: {badlink.reason}" }, 400) } # check for embeddable video domain = parsed_url.netloc if url: repost = g.db.query(Submission).join(Submission.submission_aux).filter( SubmissionAux.url.ilike(url), Submission.board_id == board.id, Submission.deleted_utc == 0, Submission.is_banned == False).order_by( Submission.id.asc()).first() else: repost = None if repost and request.values.get("no_repost"): return redirect(repost.permalink) if request.files.get('file') and not v.can_submit_image: abort(403) # offensive is_offensive = False for x in g.db.query(BadWord).all(): if (body and x.check(body)) or x.check(title): is_offensive = True break new_post = Submission(author_id=v.id, domain_ref=domain_obj.id if domain_obj else None, board_id=board.id, original_board_id=board.id, over_18=(bool(request.form.get("over_18", "")) or board.over_18), post_public=not board.is_private, repost_id=repost.id if repost else None, is_offensive=is_offensive, app_id=v.client.application.id if v.client else None, creation_region=request.headers.get("cf-ipcountry"), is_bot=request.headers.get("X-User-Type") == "Bot") g.db.add(new_post) g.db.flush() new_post_aux = SubmissionAux(id=new_post.id, url=url, body=body, body_html=body_html, embed_url=embed, title=title) g.db.add(new_post_aux) g.db.flush() vote = Vote(user_id=v.id, vote_type=1, submission_id=new_post.id) g.db.add(vote) g.db.flush() g.db.refresh(new_post) # check for uploaded image if request.files.get('file'): #check file size if request.content_length > 16 * 1024 * 1024 and not v.has_premium: g.db.rollback() abort(413) file = request.files['file'] if not file.content_type.startswith('image/'): return { "html": lambda: (render_template("submit.html", v=v, error=f"Image files only.", title=title, body=request.form.get("body", ""), b=board), 400), "api": lambda: ({ "error": f"Image files only" }, 400) } name = f'post/{new_post.base36id}/{secrets.token_urlsafe(8)}' upload_file(name, file) # thumb_name=f'posts/{new_post.base36id}/thumb.png' #upload_file(name, file, resize=(375,227)) # update post data new_post.url = f'https://{BUCKET}/{name}' new_post.is_image = True new_post.domain_ref = 1 # id of i.ruqqus.com domain g.db.add(new_post) #csam detection def del_function(db): delete_file(name) new_post.is_banned = True db.add(new_post) db.commit() ma = ModAction(kind="ban_post", user_id=1, note="banned image", target_submission_id=new_post.id) db.add(ma) db.commit() csam_thread = threading.Thread( target=check_csam_url, args=(f"https://{BUCKET}/{name}", v, lambda: del_function(db=db_session()))) csam_thread.start() g.db.commit() # spin off thumbnail generation and csam detection as new threads if (new_post.url or request.files.get('file')) and ( v.is_activated or request.headers.get('cf-ipcountry') != "T1"): new_thread = threading.Thread(target=thumbnail_thread, args=(new_post.base36id, )) new_thread.start() # expire the relevant caches: front page new, board new cache.delete_memoized(frontlist) g.db.commit() cache.delete_memoized(Board.idlist, board, sort="new") # print(f"Content Event: @{new_post.author.username} post # {new_post.base36id}") return { "html": lambda: redirect(new_post.permalink), "api": lambda: jsonify(new_post.json) }
def dmca_post(v): data = {x: request.form[x] for x in request.form if x != "formkey"} email_text = render_template("help/dmca_email.md", v=v, **data) with CustomRenderer() as renderer: email_html = renderer.render(mistletoe.Document(email_text)) email_html = sanitize(email_html, linkgen=True) try: send_mail(environ.get("admin_email"), "DMCA Takedown Request", email_html) except BaseException: return render_template( "/help/dmca.html", error="Unable to save your request. Please try again later.", v=v) post_text = render_template("help/dmca_notice.md", v=v, **data) with CustomRenderer() as renderer: post_html = renderer.render(mistletoe.Document(post_text)) post_html = sanitize(post_html, linkgen=True) # create +RuqqusDMCA post new_post = Submission(author_id=1, domain_ref=None, board_id=1000, original_board_id=1000, over_18=False, post_public=True, repost_id=None, is_offensive=False) g.db.add(new_post) g.db.flush() new_post_aux = SubmissionAux(id=new_post.id, url=None, body=post_text, body_html=post_html, embed_url=None, title=f"DMCA {new_post.base36id}") g.db.add(new_post_aux) g.db.flush() comment_text = f"##### Username\n\n@{v.username}\n\n##### Email\n\n{v.email}\n\n##### Address\n\n{data['your_address']}" with CustomRenderer() as renderer: c_html = renderer.render(mistletoe.Document(comment_text)) c_html = sanitize(c_html, linkgen=True) c = Comment(author_id=1, parent_submission=new_post.id, parent_fullname=new_post.fullname, parent_comment_id=None, level=1, over_18=False, is_nsfl=False, is_op=True, is_offensive=False, original_board_id=1000, is_deleted=True) g.db.add(c) g.db.flush() c_aux = CommentAux(id=c.id, body_html=c_html, body=comment_text) g.db.add(c_aux) g.db.commit() return render_template("/help/dmca.html", msg="Your request has been saved.", v=v)
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.lstrip().rstrip() if not body and not (v.has_premium and request.files.get('file')): return jsonify({"error":"You need to actually write something!"}), 400 body=preprocess(body) 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 # No commenting on deleted/removed things if parent.is_banned or parent.deleted_utc > 0: return jsonify( {"error": "You can't comment on things that have been deleted."}), 403 if parent.author.any_block_exists(v) and not v.admin_level>=3 and not parent.post.board.has_mod(v, "content"): 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(v, text) v.ban(reason="Spamming.", days=1) for alt in v.alts: if not alt.is_suspended: alt.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) ma=ModAction( user_id=1, 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 badwords=g.db.query(BadWord).all() if badwords: for x in badwords: if x.check(body): is_offensive = True break else: is_offensive = False else: is_offensive=False # 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 # 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, is_nsfl=post.is_nsfl, is_offensive=is_offensive, original_board_id=parent_post.board_id, is_bot=is_bot, app_id=v.client.application.id if v.client else None, creation_region=request.headers.get("cf-ipcountry") ) g.db.add(c) g.db.flush() if v.has_premium: 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)}' upload_file(name, file) body = request.form.get("body") + f"\n\n![](https://{BUCKET}/{name})" body=preprocess(body) 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://{BUCKET}/{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() notify_users = set() # queue up notification for parent author if parent.author.id != v.id: notify_users.add(parent.author.id) # queue up notifications for username mentions soup = BeautifulSoup(body_html, features="html.parser") mentions = soup.find_all("a", href=re.compile("^/@(\w+)"), limit=3) 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) # create auto upvote vote = CommentVote(user_id=v.id, comment_id=c.id, vote_type=1 ) g.db.add(vote) c.post.score_activity = c.post.rank_activity g.db.add(c.post) g.db.commit() c=get_comment(c.id, v=v) # print(f"Content Event: @{v.username} comment {c.base36id}") 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 }
def markdown_render(text, url, xref, toc): with DocRenderer(url, xref, toc) as renderer: return renderer.render(mistletoe.Document(text))
def submit_post(v): title=request.form.get("title","") title=title.lstrip().rstrip() url=request.form.get("url","") board=get_guild(request.form.get('board','general'), graceful=True) if not board: board=get_guild('general') if not title: return render_template("submit.html", v=v, error="Please enter a better title.", title=title, url=url, body=request.form.get("body",""), b=board ) # if len(title)<10: # return render_template("submit.html", # v=v, # error="Please enter a better title.", # title=title, # url=url, # body=request.form.get("body",""), # b=board # ) elif len(title)>500: return render_template("submit.html", v=v, error="500 character limit for titles.", title=title[0:500], url=url, body=request.form.get("body",""), b=board ) parsed_url=urlparse(url) if not (parsed_url.scheme and parsed_url.netloc) and not request.form.get("body") and not request.files.get("file",None): return render_template("submit.html", v=v, error="Please enter a URL or some text.", title=title, url=url, body=request.form.get("body",""), b=board ) #sanitize title title=bleach.clean(title) #Force https for submitted urls if request.form.get("url"): new_url=ParseResult(scheme="https", netloc=parsed_url.netloc, path=parsed_url.path, params=parsed_url.params, query=parsed_url.query, fragment=parsed_url.fragment) url=urlunparse(new_url) else: url="" body=request.form.get("body","") #check for duplicate dup = g.db.query(Submission).join(Submission.submission_aux).filter( Submission.author_id==v.id, Submission.is_deleted==False, Submission.board_id==board.id, SubmissionAux.title==title, SubmissionAux.url==url, SubmissionAux.body==body ).first() if dup: return redirect(dup.permalink) #check for domain specific rules parsed_url=urlparse(url) domain=parsed_url.netloc # check ban status domain_obj=get_domain(domain) if domain_obj: if not domain_obj.can_submit: return render_template("submit.html", v=v, error=BAN_REASONS[domain_obj.reason], title=title, url=url, body=request.form.get("body",""), b=get_guild(request.form.get("board","general"), graceful=True) ) #check for embeds if domain_obj.embed_function: try: embed=eval(domain_obj.embed_function)(url) except: embed="" else: embed="" else: embed="" #board board_name=request.form.get("board","general") board_name=board_name.lstrip("+") board_name=board_name.rstrip() board=get_guild(board_name, graceful=True) if not board: board=get_guild('general') if board.is_banned: return render_template("submit.html", v=v, error=f"+{board.name} has been demolished.", title=title, url=url , body=request.form.get("body",""), b=get_guild("general", graceful=True) ), 403 if board.has_ban(v): return render_template("submit.html", v=v, error=f"You are exiled from +{board.name}.", title=title, url=url , body=request.form.get("body",""), b=get_guild("general") ), 403 if (board.restricted_posting or board.is_private) and not (board.can_submit(v)): return render_template("submit.html", v=v, error=f"You are not an approved contributor for +{board.name}.", title=title, url=url, body=request.form.get("body",""), b=get_guild(request.form.get("board","general"), graceful=True ) ) #check spam now=int(time.time()) cutoff=now-60*60*24 similar_posts = g.db.query(Submission).options( lazyload('*') ).join(Submission.submission_aux ).filter( Submission.author_id==v.id, SubmissionAux.title.op('<->')(title)<app.config["SPAM_SIMILARITY_THRESHOLD"], Submission.created_utc>cutoff ).all() if url: similar_urls=g.db.query(Submission).options( lazyload('*') ).join(Submission.submission_aux ).filter( Submission.author_id==v.id, SubmissionAux.url.op('<->')(url)<app.config["SPAM_URL_SIMILARITY_THRESHOLD"], Submission.created_utc>cutoff ).all() else: similar_urls=[] 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 max(len(similar_urls), len(similar_posts)) >= threshold: text="Your Ruqqus account has been suspended for 1 day for the following reason:\n\n> Too much spam!" send_notification(v, text) v.ban(reason="Spamming.", include_alts=True, days=1) for post in similar_posts+similar_urls: post.is_banned=True g.db.add(post) g.db.commit() return redirect("/notifications") #print(similar_posts) #now make new post #catch too-long body if len(str(body))>10000: return render_template("submit.html", v=v, error="10000 character limit for text body", title=title, text=str(body)[0:10000], url=url, b=get_guild(request.form.get("board","general"), graceful=True ) ), 400 if len(url)>2048: return render_template("submit.html", v=v, error="URLs cannot be over 2048 characters", title=title, text=body[0:2000], b=get_guild(request.form.get("board","general"), graceful=True) ), 400 with CustomRenderer() as renderer: body_md=renderer.render(mistletoe.Document(body)) body_html = sanitize(body_md, linkgen=True) #check for embeddable video domain=parsed_url.netloc if url: repost = g.db.query(Submission).join(Submission.submission_aux).filter( SubmissionAux.url.ilike(url), Submission.board_id==board.id, Submission.is_deleted==False, Submission.is_banned==False ).order_by( Submission.id.asc() ).first() else: repost=None if request.files.get('file') and not v.can_submit_image: abort(403) #offensive for x in g.db.query(BadWord).all(): if (body and x.check(body)) or x.check(title): is_offensive=True break else: is_offensive=False new_post=Submission(author_id=v.id, domain_ref=domain_obj.id if domain_obj else None, board_id=board.id, original_board_id=board.id, over_18=(bool(request.form.get("over_18","")) or board.over_18), post_public=not board.is_private, repost_id=repost.id if repost else None, is_offensive=is_offensive ) g.db.add(new_post) g.db.flush() new_post_aux=SubmissionAux(id=new_post.id, url=url, body=body, body_html=body_html, embed_url=embed, title=title ) g.db.add(new_post_aux) g.db.flush() vote=Vote(user_id=v.id, vote_type=1, submission_id=new_post.id ) g.db.add(vote) g.db.flush() g.db.commit() g.db.refresh(new_post) #check for uploaded image if request.files.get('file'): file=request.files['file'] name=f'post/{new_post.base36id}/{secrets.token_urlsafe(8)}' upload_file(name, file) #update post data new_post.url=f'https://{BUCKET}/{name}' new_post.is_image=True new_post.domain_ref=1 #id of i.ruqqus.com domain g.db.add(new_post) g.db.flush() #spin off thumbnail generation and csam detection as new threads elif new_post.url: new_thread=threading.Thread(target=thumbnail_thread, args=(new_post.base36id,) ) new_thread.start() csam_thread = threading.Thread(target=check_csam, args=(new_post,)) csam_thread.start() #expire the relevant caches: front page new, board new #cache.delete_memoized(frontlist, sort="new") g.db.commit() cache.delete_memoized(Board.idlist, board, sort="new") #print(f"Content Event: @{new_post.author.username} post {new_post.base36id}") return redirect(new_post.permalink)
def markdown_headers(text, url, xref): with DocPreRenderer(url, xref) as renderer: renderer.render(mistletoe.Document(text)) return renderer._toc
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) 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.lstrip().rstrip() 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}" return jsonify({"error": reason}), 401 #check existing existing = g.db.query(Comment).join(CommentAux).filter( Comment.author_id == v.id, Comment.is_deleted == False, 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 #No commenting on deleted/removed things if parent.is_banned or parent.is_deleted: return jsonify( {"error": "You can't comment on things that have been deleted."}), 403 if parent.author.any_block_exists(v): 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 #check spam - this is stupid slow for now #similar_comments=g.db.query(Comment).filter(Comment.author_id==v.id, CommentAux.body.op('<->')(body)<0.5).options(contains_eager(Comment.comment_aux)).all() #print(similar_comments) for x in g.db.query(BadWord).all(): if x.check(body): is_offensive = True break else: is_offensive = False #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 #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, is_nsfl=post.is_nsfl, is_op=(v.id == post.author_id), is_offensive=is_offensive, original_board_id=parent_post.board_id) g.db.add(c) g.db.flush() c_aux = CommentAux(id=c.id, body_html=body_html, body=body) g.db.add(c_aux) g.db.flush() notify_users = set() #queue up notification for parent author if parent.author.id != v.id: notify_users.add(parent.author.id) #queue up notifications for username mentions soup = BeautifulSoup(body_html, features="html.parser") mentions = soup.find_all("a", href=re.compile("^/@(\w+)"), limit=3) 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) #create auto upvote vote = CommentVote(user_id=v.id, comment_id=c.id, vote_type=1) g.db.add(vote) g.db.commit() #print(f"Content Event: @{v.username} comment {c.base36id}") 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 }
def submit_post(v): title = request.form.get("title", "") url = request.form.get("url", "") if len(title) < 10: return render_template("submit.html", v=v, error="Please enter a better title.") parsed_url = urlparse(url) if not (parsed_url.scheme and parsed_url.netloc) and not request.form.get("body"): return render_template("submit.html", v=v, error="Please enter a URL or some text.") #sanitize title title = sanitize(title, linkgen=False) #check for duplicate dup = db.query(Submission).filter_by(title=title, author_id=v.id, url=url).first() if dup: return redirect(dup.permalink) #check for domain specific rules domain = urlparse(url).netloc ##all possible subdomains parts = domain.split(".") domains = [] for i in range(len(parts)): new_domain = parts[i] for j in range(i + 1, len(parts)): new_domain += "." + parts[j] domains.append(new_domain) domain_obj = db.query(Domain).filter(Domain.domain.in_(domains)).first() if domain_obj: if not domain_obj.can_submit: return render_template("submit.html", v=v, error=BAN_REASONS[domain_obj.reason]) #now make new post body = request.form.get("body", "") with UserRenderer() as renderer: body_md = renderer.render(mistletoe.Document(body)) body_html = sanitize(body_md, linkgen=True) #check for embeddable video domain = parsed_url.netloc embed = "" if domain.endswith(("youtube.com", "youtu.be")): embed = youtube_embed(url) new_post = Submission(title=title, url=url, author_id=v.id, body=body, body_html=body_html, embed_url=embed, domain_ref=domain_obj.id if domain_obj else None) db.add(new_post) db.commit() vote = Vote(user_id=v.id, vote_type=1, submission_id=new_post.id) db.add(vote) db.commit() return redirect(new_post.permalink)
def settings_profile_post(v): updated = False if request.values.get("over18", v.over_18) != v.over_18: updated = True v.over_18 = request.values.get("over18", None) == 'true' cache.delete_memoized(User.idlist, v) if request.values.get("hide_offensive", v.hide_offensive) != v.hide_offensive: updated = True v.hide_offensive = request.values.get("hide_offensive", None) == 'true' cache.delete_memoized(User.idlist, v) if request.values.get("show_nsfl", v.show_nsfl) != v.show_nsfl: updated = True v.show_nsfl = request.values.get("show_nsfl", None) == 'true' cache.delete_memoized(User.idlist, v) if request.values.get("filter_nsfw", v.filter_nsfw) != v.filter_nsfw: updated = True v.filter_nsfw = not request.values.get("filter_nsfw", None) == 'true' cache.delete_memoized(User.idlist, v) if request.values.get("private", v.is_private) != v.is_private: updated = True v.is_private = request.values.get("private", None) == 'true' if request.values.get("bio"): bio = request.values.get("bio")[0:256] if bio == v.bio: return render_template("settings_profile.html", v=v, error="You didn't change anything") v.bio = bio with CustomRenderer() as renderer: v.bio_html = renderer.render(mistletoe.Document(bio)) v.bio_html = sanitize(v.bio_html, linkgen=True) g.db.add(v) return render_template("settings_profile.html", v=v, msg="Your bio has been updated.") x = request.values.get("title_id", None) if x: x = int(x) if x == 0: v.title_id = None updated = True elif x > 0: title = get_title(x) if bool(eval(title.qualification_expr)): v.title_id = title.id updated = True else: return jsonify({ "error": f"You don't meet the requirements for title `{title.text}`." }), 403 else: abort(400) if updated: g.db.add(v) return jsonify({"message": "Your settings have been updated."}) else: return jsonify({"error": "You didn't change anything."}), 400
def settings_profile_post(v): updated = False if request.values.get("over18", v.over_18) != v.over_18: updated = True v.over_18 = request.values.get("over18", None) == 'true' cache.delete_memoized(User.idlist, v) if request.values.get("hide_offensive", v.hide_offensive) != v.hide_offensive: updated = True v.hide_offensive = request.values.get("hide_offensive", None) == 'true' cache.delete_memoized(User.idlist, v) if request.values.get("hide_bot", v.hide_bot) != v.hide_bot: updated = True v.hide_bot = request.values.get("hide_bot", None) == 'true' cache.delete_memoized(User.idlist, v) if request.values.get("show_nsfl", v.show_nsfl) != v.show_nsfl: updated = True v.show_nsfl = request.values.get("show_nsfl", None) == 'true' cache.delete_memoized(User.idlist, v) if request.values.get("filter_nsfw", v.filter_nsfw) != v.filter_nsfw: updated = True v.filter_nsfw = not request.values.get("filter_nsfw", None) == 'true' cache.delete_memoized(User.idlist, v) if request.values.get("private", v.is_private) != v.is_private: updated = True v.is_private = request.values.get("private", None) == 'true' if request.values.get("nofollow", v.is_nofollow) != v.is_nofollow: updated = True v.is_nofollow = request.values.get("nofollow", None) == 'true' if request.values.get("join_chat", v.auto_join_chat) != v.auto_join_chat: updated = True v.auto_join_chat = request.values.get("join_chat", None) == 'true' if request.values.get("bio") is not None: bio = request.values.get("bio")[0:256] bio = preprocess(bio) if bio == v.bio: return { "html": lambda: render_template("settings_profile.html", v=v, error="You didn't change anything"), "api": lambda: jsonify({"error": "You didn't change anything"}) } with CustomRenderer() as renderer: bio_html = renderer.render(mistletoe.Document(bio)) bio_html = sanitize(bio_html, linkgen=True) # Run safety filter bans = filter_comment_html(bio_html) if bans: ban = bans[0] reason = f"Remove the {ban.domain} link from your bio 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 is not allowed.") return jsonify({"error": reason}), 401 v.bio = bio v.bio_html = bio_html g.db.add(v) return { "html": lambda: render_template("settings_profile.html", v=v, msg="Your bio has been updated."), "api": lambda: jsonify({"message": "Your bio has been updated."}) } if request.values.get("filters") is not None: filters = request.values.get("filters")[0:1000].lstrip().rstrip() if filters == v.custom_filter_list: return { "html": lambda: render_template("settings_profile.html", v=v, error="You didn't change anything"), "api": lambda: jsonify({"error": "You didn't change anything"}) } v.custom_filter_list = filters g.db.add(v) return { "html": lambda: render_template("settings_profile.html", v=v, msg= "Your custom filters have been updated."), "api": lambda: jsonify( {"message": "Your custom filters have been updated"}) } x = request.values.get("title_id", None) if x: x = int(x) if x == 0: v.title_id = None updated = True elif x > 0: title = get_title(x) if bool(eval(title.qualification_expr)): v.title_id = title.id updated = True else: return jsonify({ "error": f"You don't meet the requirements for title `{title.text}`." }), 403 else: abort(400) defaultsorting = request.values.get("defaultsorting") if defaultsorting: if defaultsorting in [ "hot", "new", "old", "activity", "disputed", "top" ]: v.defaultsorting = defaultsorting updated = True else: abort(400) defaulttime = request.values.get("defaulttime") if defaulttime: if defaulttime in ["day", "week", "month", "year", "all"]: v.defaulttime = defaulttime updated = True else: abort(400) if updated: g.db.add(v) return jsonify({"message": "Your settings have been updated."}) else: return jsonify({"error": "You didn't change anything."}), 400
def api_comment(v): parent_submission=base36decode(request.form.get("submission")) parent_fullname=request.form.get("parent_fullname") #process and sanitize body=request.form.get("body","")[0:10000] with CustomRenderer(post_id=request.form.get("submission")) 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: return render_template("comment_failed.html", action="/api/comment", parent_submission=request.form.get("submission"), parent_fullname=request.form.get("parent_fullname"), badlinks=[x.domain for x in bans], body=body, v=v ), 422 #check existing existing=db.query(Comment).filter_by(author_id=v.id, body=body, parent_fullname=parent_fullname, parent_submission=parent_submission ).first() if existing: return redirect(existing.permalink) #get parent item info parent_id=int(parent_fullname.split("_")[1], 36) if parent_fullname.startswith("t2"): parent=db.query(Submission).filter_by(id=parent_id).first() parent_comment_id=None level=1 elif parent_fullname.startswith("t3"): parent=db.query(Comment).filter_by(id=parent_id).first() parent_comment_id=parent.id level=parent.level+1 #No commenting on deleted/removed things if parent.is_banned or parent.is_deleted: abort(403) #check for ban state post = get_post(request.form.get("submission")) if post.is_archived or not post.board.can_comment(v): abort(403) #create comment c=Comment(author_id=v.id, body=body, body_html=body_html, parent_submission=parent_submission, parent_fullname=parent_fullname, parent_comment_id=parent_comment_id, level=level, author_name=v.username, over_18=post.over_18, is_nsfl=post.is_nsfl, is_op=(v.id==post.author_id) ) db.add(c) db.commit() c.determine_offensive() notify_users=set() #queue up notification for parent author if parent.author.id != v.id: notify_users.add(parent.author.id) #queue up notifications for username mentions soup=BeautifulSoup(c.body_html, features="html.parser") mentions=soup.find_all("a", href=re.compile("^/@(\w+)"), limit=3) for mention in mentions: username=mention["href"].split("@")[1] user=db.query(User).filter_by(username=username).first() if user: notify_users.add(user.id) for x in notify_users: n=Notification(comment_id=c.id, user_id=x) db.add(n) db.commit() #create auto upvote vote=CommentVote(user_id=v.id, comment_id=c.id, vote_type=1 ) db.add(vote) db.commit() #print(f"Content Event: @{v.username} comment {c.base36id}") return redirect(f"{c.permalink}?context=1")
def submit_post(v): title = request.form.get("title", "") url = request.form.get("url", "") if len(title) < 10: return render_template("submit.html", v=v, error="Please enter a better title.") parsed_url = urlparse(url) if not (parsed_url.scheme and parsed_url.netloc) and not request.form.get("body"): return render_template("submit.html", v=v, error="Please enter a URL or some text.") #sanitize title title = sanitize(title, linkgen=False) #check for duplicate dup = db.query(Submission).filter_by(title=title, author_id=v.id, url=url).first() if dup: return redirect(dup.permalink) #check for domain specific rules parsed_url = urlparse(url) domain = parsed_url.netloc ##all possible subdomains parts = domain.split(".") domains = [] for i in range(len(parts)): new_domain = parts[i] for j in range(i + 1, len(parts)): new_domain += "." + parts[j] domains.append(new_domain) domain_obj = db.query(Domain).filter(Domain.domain.in_(domains)).first() if domain_obj: if not domain_obj.can_submit: return render_template("submit.html", v=v, error=BAN_REASONS[domain_obj.reason]) #Huffman-Ohanian growth method if v.admin_level >= 2: name = request.form.get("username", None) if name: identity = db.query(User).filter(User.username.ilike(name)).first() if not identity: if not re.match("^\w{5,25}$", name): abort(422) identity = User(username=name, password=secrets.token_hex(16), email=None, created_utc=int(time.time()), creation_ip=request.remote_addr) identity.passhash = v.passhash db.add(identity) db.commit() new_alt = Alt(user1=v.id, user2=identity.id) new_badge = Badge(user_id=identity.id, badge_id=1) db.add(new_alt) db.add(new_badge) db.commit() else: if identity not in v.alts: abort(403) user_id = identity.id else: user_id = v.id else: user_id = v.id #Force https for submitted urls if request.form.get("url"): new_url = ParseResult(scheme="https", netloc=parsed_url.netloc, path=parsed_url.path, params=parsed_url.params, query=parsed_url.query, fragment=parsed_url.fragment) url = urlunparse(new_url) else: url = "" #now make new post body = request.form.get("body", "") with UserRenderer() as renderer: body_md = renderer.render(mistletoe.Document(body)) body_html = sanitize(body_md, linkgen=True) #check for embeddable video domain = parsed_url.netloc embed = "" if domain.endswith(("youtube.com", "youtu.be")): embed = youtube_embed(url) new_post = Submission(title=title, url=url, author_id=user_id, body=body, body_html=body_html, embed_url=embed, domain_ref=domain_obj.id if domain_obj else None) db.add(new_post) db.commit() vote = Vote(user_id=user_id, vote_type=1, submission_id=new_post.id) db.add(vote) db.commit() return redirect(new_post.permalink)
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) if c.board.has_ban(v): abort(403) body = request.form.get("body", "")[0:10000] body=preprocess(body) with CustomRenderer(post_id=c.post.base36id) 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." #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) } for x in g.db.query(BadWord).all(): if x.check(body): c.is_offensive = True break else: c.is_offensive = False # 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(v, text) v.ban(reason="Spamming.", include_alts=True, 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 c.body = body c.body_html = body_html c.edited_utc = int(time.time()) g.db.add(c) g.db.commit() path = request.form.get("current_page", "/") return jsonify({"html": c.body_html})
def ast(self): if self._ast_outdated: self._ast = mistletoe.ast_renderer.get_ast( mistletoe.Document(self.text)) self._ast_outdated = False return self._ast
def edit_post(pid, v): p = get_post(pid) if not p.author_id == v.id: abort(403) if p.is_banned: abort(403) if p.board.has_ban(v): abort(403) body = request.form.get("body", "") body = preprocess(body) with CustomRenderer() 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 post 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 is not allowed.") abort(403) return {"error": reason}, 403 # check spam 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: if badlink.autoban: text = "Your Ruqqus account has been suspended for 1 day for the following reason:\n\n> Too much spam!" send_notification(v, text) v.ban(days=1, reason="spam") return redirect('/notifications') else: return { "error": f"The link `{badlink.link}` is not allowed. Reason: {badlink.reason}" } p.body = body p.body_html = body_html p.edited_utc = int(time.time()) # offensive p.is_offensive = False for x in g.db.query(BadWord).all(): if (p.body and x.check(p.body)) or x.check(p.title): p.is_offensive = True break g.db.add(p) return redirect(p.permalink)
def __init__(self, path: Path): self.doc = mistletoe.Document(path.read_text())
def submit_post(v): title = request.form.get("title", "") url = request.form.get("url", "") board = get_guild(request.form.get('board', 'general'), graceful=True) if not board: board = get_guild('general') if re.match('^\s*$', title): return render_template("submit.html", v=v, error="Please enter a better title.", title=title, url=url, body=request.form.get("body", ""), b=board) # if len(title)<10: # return render_template("submit.html", # v=v, # error="Please enter a better title.", # title=title, # url=url, # body=request.form.get("body",""), # b=board # ) elif len(title) > 500: return render_template("submit.html", v=v, error="500 character limit for titles.", title=title[0:500], url=url, body=request.form.get("body", ""), b=board) parsed_url = urlparse(url) if not (parsed_url.scheme and parsed_url.netloc) and not request.form.get( "body") and not request.files.get("file", None): return render_template("submit.html", v=v, error="Please enter a URL or some text.", title=title, url=url, body=request.form.get("body", ""), b=board) #sanitize title title = sanitize(title, linkgen=False) #check for duplicate dup = db.query(Submission).filter_by(title=title, author_id=v.id, url=url, is_deleted=False, board_id=board.id).first() if dup: return redirect(dup.permalink) #check for domain specific rules parsed_url = urlparse(url) domain = parsed_url.netloc # check ban status domain_obj = get_domain(domain) if domain_obj: if not domain_obj.can_submit: return render_template("submit.html", v=v, error=BAN_REASONS[domain_obj.reason], title=title, url=url, body=request.form.get("body", ""), b=get_guild(request.form.get( "board", "general"), graceful=True)) #check for embeds if domain_obj.embed_function: try: embed = eval(domain_obj.embed_function)(url) except: embed = "" else: embed = "" else: embed = "" #board board_name = request.form.get("board", "general") board_name = board_name.lstrip("+") board_name = board_name.rstrip() board = get_guild(board_name, graceful=True) if not board: board = get_guild('general') if board.is_banned: return render_template("submit.html", v=v, error=f"+{board.name} has been demolished.", title=title, url=url, body=request.form.get("body", ""), b=get_guild("general", graceful=True)), 403 if board.has_ban(v): return render_template("submit.html", v=v, error=f"You are exiled from +{board.name}.", title=title, url=url, body=request.form.get("body", ""), b=get_guild("general")), 403 if (board.restricted_posting or board.is_private) and not (board.can_submit(v)): return render_template( "submit.html", v=v, error=f"You are not an approved contributor for +{board.name}.", title=title, url=url, body=request.form.get("body", ""), b=get_guild(request.form.get("board", "general"), graceful=True)) user_id = v.id user_name = v.username #Force https for submitted urls if request.form.get("url"): new_url = ParseResult(scheme="https", netloc=parsed_url.netloc, path=parsed_url.path, params=parsed_url.params, query=parsed_url.query, fragment=parsed_url.fragment) url = urlunparse(new_url) else: url = "" #now make new post body = request.form.get("body", "") #catch too-long body if len(str(body)) > 10000: return render_template("submit.html", v=v, error="10000 character limit for text body", title=title, text=str(body)[0:10000], url=url, b=get_guild(request.form.get( "board", "general"), graceful=True)), 400 if len(url) > 2048: return render_template("submit.html", v=v, error="URLs cannot be over 2048 characters", title=title, text=body[0:2000], b=get_guild(request.form.get( "board", "general"), graceful=True)), 400 with CustomRenderer() as renderer: body_md = renderer.render(mistletoe.Document(body)) body_html = sanitize(body_md, linkgen=True) #check for embeddable video domain = parsed_url.netloc if url: repost = db.query(Submission).filter( Submission.url.ilike(url)).filter_by( board_id=board.id, is_deleted=False, is_banned=False).order_by(Submission.id.asc()).first() else: repost = None if request.files.get('file') and not v.can_submit_image: abort(403) new_post = Submission( title=title, url=url, author_id=user_id, body=body, body_html=body_html, embed_url=embed, domain_ref=domain_obj.id if domain_obj else None, board_id=board.id, original_board_id=board.id, over_18=(bool(request.form.get("over_18", "")) or board.over_18), post_public=not board.is_private, #author_name=user_name, #guild_name=board.name, repost_id=repost.id if repost else None) db.add(new_post) db.commit() new_post.determine_offensive() vote = Vote(user_id=user_id, vote_type=1, submission_id=new_post.id) db.add(vote) db.commit() #check for uploaded image if request.files.get('file'): file = request.files['file'] name = f'post/{new_post.base36id}/{secrets.token_urlsafe(8)}' upload_file(name, file) #update post data new_post.url = f'https://{BUCKET}/{name}' new_post.is_image = True new_post.domain_ref = 1 #id of i.ruqqus.com domain db.add(new_post) db.commit() #spin off thumbnail generation and csam detection as new threads elif new_post.url: new_thread = threading.Thread(target=thumbnail_thread, args=(new_post.base36id, )) new_thread.start() csam_thread = threading.Thread(target=check_csam, args=(new_post, )) csam_thread.start() #expire the relevant caches: front page new, board new #cache.delete_memoized(frontlist, sort="new") cache.delete_memoized(Board.idlist, board, sort="new") #print(f"Content Event: @{new_post.author.username} post {new_post.base36id}") return redirect(new_post.permalink)
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) 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.lstrip().rstrip() 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}" return jsonify({"error": reason}), 401 # check existing existing = g.db.query(Comment).join(CommentAux).filter( Comment.author_id == v.id, Comment.is_deleted == False, 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 # No commenting on deleted/removed things if parent.is_banned or parent.is_deleted: return jsonify( {"error": "You can't comment on things that have been deleted."}), 403 if parent.author.any_block_exists(v): 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["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 Ruqqus account has been suspended for 1 day for the following reason:\n\n> Too much spam!" send_notification(v, text) v.ban(reason="Spamming.", include_alts=True, 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 badwords = g.db.query(BadWord).all() if badwords: for x in badwords: if x.check(body): is_offensive = True break else: is_offensive = False else: is_offensive = False # 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 # 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, is_nsfl=post.is_nsfl, is_op=(v.id == post.author_id), is_offensive=is_offensive, original_board_id=parent_post.board_id, is_bot=is_bot) g.db.add(c) g.db.flush() c_aux = CommentAux(id=c.id, body_html=body_html, body=body) g.db.add(c_aux) g.db.flush() notify_users = set() # queue up notification for parent author if parent.author.id != v.id: notify_users.add(parent.author.id) # queue up notifications for username mentions soup = BeautifulSoup(body_html, features="html.parser") mentions = soup.find_all("a", href=re.compile("^/@(\w+)"), limit=3) 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) # create auto upvote vote = CommentVote(user_id=v.id, comment_id=c.id, vote_type=1) g.db.add(vote) c.post.score_activity = c.post.rank_activity g.db.add(c.post) g.db.commit() # print(f"Content Event: @{v.username} comment {c.base36id}") 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 }
def create_board_post(v): if not v.can_make_guild: return render_template( "make_board.html", title="Unable to make board", error="You need more Reputation before you can make a Guild.") board_name = request.form.get("name") board_name = board_name.lstrip("+") description = request.form.get("description") if not re.match(valid_board_regex, board_name): return render_template( "make_board.html", v=v, error="Guild names must be 3-25 letters or numbers.", description=description) # check name if get_guild(board_name, graceful=True): return render_template("make_board.html", v=v, error="That Guild already exists.", description=description) # check # recent boards made by user cutoff = int(time.time()) - 60 * 60 * 24 recent = g.db.query(Board).filter(Board.creator_id == v.id, Board.created_utc >= cutoff).all() if len([x for x in recent]) >= 2: return render_template( "message.html", title="You need to wait a bit.", message= "You can only create up to 2 guilds per day. Try again later." ), 429 with CustomRenderer() as renderer: description_md = renderer.render(mistletoe.Document(description)) description_html = sanitize(description_md, linkgen=True) # make the board new_board = Board(name=board_name, description=description, description_html=description_html, over_18=bool(request.form.get("over_18", "")), creator_id=v.id) g.db.add(new_board) g.db.commit() # add user as mod mod = ModRelationship(user_id=v.id, board_id=new_board.id, accepted=True) g.db.add(mod) # add subscription for user sub = Subscription(user_id=v.id, board_id=new_board.id) g.db.add(sub) # clear cache cache.delete_memoized(guild_ids, sort="new") return redirect(new_board.permalink)
def md_to_latex(value): with LaTeXRenderer() as renderer: return renderer.render_inner(mistletoe.Document(value))