def edit_comment(cid, v): c = get_comment(cid) if not c.author_id == v.id: abort(403) if c.is_banned or c.is_deleted: abort(403) if c.board.has_ban(v): abort(403) body = request.form.get("body", "")[0:10000] 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: 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 blacklist domain was used.' } } c.body = body c.body_html = body_html c.edited_utc = int(time.time()) g.db.add(c) c.determine_offensive() path = request.form.get("current_page", "/") return redirect(f"{path}#comment-{c.base36id}")
def api_comment(v): parent_submission = base36decode(request.form.get("submission")) parent_fullname = request.form.get("parent_fullname") #sanitize body = request.form.get("body", "") body_md = mistletoe.markdown(body) body_html = sanitize(body_md, linkgen=True) #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) parent_id = int(parent_fullname.split("_")[1], 36) if parent_fullname.startswith("t2"): parent = db.query(Submission).filter_by(id=parent_id).first() elif parent_fullname.startswith("t3"): parent = db.query(Comment).filter_by(id=parent_id).first() if parent.is_banned: abort(403) c = Comment(author_id=v.id, body=body, body_html=body_html, parent_submission=parent_submission, parent_fullname=parent_fullname, parent_author_id=parent.author.id if parent.author.id != v.id else None) db.add(c) db.commit() vote = CommentVote(user_id=v.id, comment_id=c.id, vote_type=1) db.add(vote) db.commit() return redirect(f"{c.post.permalink}#comment-{c.base36id}")
def edit_comment(v): comment_id = request.form.get("id") body = request.form.get("comment", "") body_md = mistletoe.markdown(body) body_html = sanitize(body_md, linkgen=True) c = db.query(Comment).filter_by(id=comment_id, author_id=v.id).first() if not c: abort(404) c.body = body c.body_html = body_html c.edited_timestamp = time.time() db.add(c) db.commit()
def direct_message(args, guild, v): """Send someone a private message. Use Tab to reply to the most recent private message""" if len(args) < 3: send("Not enough arguments. Type `/help msg` for more information.") return user = get_user(args[1], graceful=True) if not user: send(f"No user named @{args[1]}.") return targets = SIDS.get(user.id, []) if not targets: send(f"@{user.username} is not online right now.") return text = " ".join(args[2:]) text = preprocess(text) with CustomRenderer() as renderer: text = renderer.render(mistletoe.Document(text)) text = sanitize(text, linkgen=True) t = now() data = { "avatar": v.profile_url, "username": v.username, "text": text, "time": t } for sid in targets: emit('msg-in', data, to=sid) data = { "avatar": v.profile_url, "username": user.username, "text": text, "time": t } for sid in SIDS.get(v.id, []): emit('msg-out', data, to=sid)
def speak(text, user, guild, as_guild=False, event="speak", to=None): if isinstance(text, list): text = " ".join(text) text = preprocess(text) with CustomRenderer() as renderer: text = renderer.render(mistletoe.Document(text)) text = sanitize(text, linkgen=True) to = to or guild.fullname ban = screen(text) if ban: speak_help(f"Unable to send message - banned domain {ban}") return if as_guild or event == "motd": data = { "avatar": guild.profile_url, "username": guild.name, "text": text, "room": guild.fullname, "guild": guild.name, "time": now(), 'userlink': guild.permalink } emit("motd", data, to=to) else: data = { "avatar": user.profile_url, "username": user.username, "text": text, "room": guild.fullname, "guild": guild.name, "time": now(), "userlink": user.permalink } if request.headers.get("X-User-Type", "").lower() == "bot": emit("bot", data, to=to) else: emit(event, data, to=to) return
def speak_as_gm(args, guild, v): """Distinguish your message with a Guildmaster's crown. (Must be Guildmaster.)""" text = " ".join(args[1:]) if not text: return text = preprocess(text) with CustomRenderer() as renderer: text = renderer.render(mistletoe.Document(text)) text = sanitize(text, linkgen=True) data = { "avatar": v.profile_url, "username": v.username, "text": text, "room": guild.fullname, "guild": guild.name, "time": now() } emit('gm', data, to=guild.fullname)
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)) if request.form.get("bio") != v.bio: updated = True bio = request.form.get("bio") v.bio = bio v.bio_html = sanitize(bio) if updated: db.add(v) db.commit() 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", "") url = request.form.get("url", "") if len(title) < 10: return render_template("submit.html", v=v, error="Please enter a better title.") x = urlparse(url) if not (x.scheme and x.netloc): return render_template("submit.html", v=v, error="Please enter a URL.") #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) #now make new post new_post = Submission(title=title, url=url, author_id=v.id) #run through content filter x = filter_post(new_post) if x: return render_template("submit.html", v=v, error=x) 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 api_comment(v): body = request.form.get("text") parent_fullname = request.form.get("parent_fullname") #sanitize body = request.form.get("body") body_html = mistletoe.markdown(body_md) body_html = sanitize(body_html, linkgen=True) #check existing existing = db.query(Comment).filter_by( author_id=v.id, body=body, parent_fullname=parent_fullname).first() if existing: return redirect(existing.permalink) c = Comment(author_id=v.id, body=body, body_html=body_html) db.add(c) db.commit()
def speak_admin(args, guild, v): """Distinguish your message with an Administrator's shield. (Must be site administrator.)""" text = " ".join(args[1:]) if not text: return text = preprocess(text) with CustomRenderer() as renderer: text = renderer.render(mistletoe.Document(text)) text = sanitize(text, linkgen=True) data = { "avatar": v.profile_url, "username": v.username, "text": text, 'guild': guild.name, 'room': guild.fullname, "time": now() } emit('admin', data, to=guild.fullname)
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) 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 # politics p.is_politics = False for x in g.db.query(PoliticsWord).all(): if (p.body and x.check(p.body)) or x.check(p.title): p.is_politics = True break g.db.add(p) return redirect(p.permalink)
def mod_add_rule(bid, board, v): # board description rule = request.form.get("rule1") rule2 = request.form.get("rule2") if not rule2: with CustomRenderer() as renderer: rule_md = renderer.render(mistletoe.Document(rule)) rule_html = sanitize(rule_md, linkgen=True) new_rule = Rules(board_id=bid, rule_body=rule, rule_html=rule_html) g.db.add(new_rule) else: """ im guessing here we should do a loop for adding multiple rules """ pass return "", 204
def wallop(args, guild, v): """Send a global broadcast. (Must be site administrator.)""" text = " ".join(args[1:]) if not text: return text = preprocess(text) with CustomRenderer() as renderer: text = renderer.render(mistletoe.Document(text)) text = sanitize(text, linkgen=True) data = { "avatar": v.profile_url, "username": v.username, "text": text, "time": now() } sent = [] for uid in SIDS: for sid in SIDS[uid]: for roomid in rooms(sid=sid): if roomid.startswith('t4_') and roomid not in sent: emit('wallop', data, to=roomid) sent.append(roomid)
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("nofollow", v.is_nofollow) != v.is_nofollow: updated = True v.is_nofollow = request.values.get("nofollow", None) == 'true' if request.values.get("bio") is not None: 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 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 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 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", "") 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 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)) #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: 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 #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: return render_template( "submit.html", v=v, error= f"The link `{badlink.link}` is not allowed. Reason: {badlink.reason}", title=title, text=body[0:2000], b=get_guild(request.form.get("board", "general"), graceful=True)), 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.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 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") #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 api_comment(v): parent_submission = base36decode(request.form.get("submission")) parent_fullname = request.form.get("parent_fullname") parent_post = get_post(request.form.get("submission")) #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 = parent_post 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).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(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 #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 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: 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: 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=g.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=g.db.query(Submission).filter_by(id=parent_id).first() parent_comment_id=None level=1 elif parent_fullname.startswith("t3"): parent=g.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) ) c.determine_offensive() g.db.add(c) g.db.commit() g.db.begin() 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: 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 redirect(f"{c.permalink}?context=1")
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: return { "html": lambda: (render_template("submit.html", v=v, error=f"Please enter a Guild to submit to.", title=title, url=url, body=request.form.get("body", ""), b=None), 403), "api": lambda: (jsonify( {"error": f"403 Forbidden - +{board.name} has been banned."})) } 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=None), 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=None), 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=None), 403), "api": lambda: (jsonify({ "error": f"403 Not Authorized - You are not an approved contributor for +{board.name}" }), 403) } if board.disallowbots and request.headers.get("X-User-Type") == "Bot": return { "api": lambda: (jsonify({ "error": f"403 Not Authorized - +{board.name} disallows bots from posting and commenting!" }), 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", "").lower() == "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) g.db.add(new_post.submission_aux) g.db.commit() #csam detection def del_function(): db = db_session() 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() db.close() csam_thread = threading.Thread(target=check_csam_url, args=(f"https://{BUCKET}/{name}", v, del_function)) 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 = gevent.spawn(thumbnail_thread, new_post.base36id) # expire the relevant caches: front page new, board new cache.delete_memoized(frontlist) g.db.commit() cache.delete_memoized(Board.idlist, board, sort="new") # queue up notifications for username mentions notify_users = set() soup = BeautifulSoup(body_html, features="html.parser") for mention in soup.find_all("a", href=re.compile("^/@(\w+)"), limit=3): username = mention["href"].split("@")[1] user = g.db.query(User).filter_by(username=username).first() if user and not v.any_block_exists(user) and user.id != v.id: notify_users.add(user.id) for x in notify_users: send_notification( x, f"@{v.username} has mentioned you: https://ruqqus.com{new_post.permalink}" ) # print(f"Content Event: @{new_post.author.username} post # {new_post.base36id}") #Bell notifs board_uids = g.db.query(Subscription.user_id).options( lazyload('*')).filter( Subscription.board_id == new_post.board_id, Subscription.is_active == True, Subscription.get_notifs == True, Subscription.user_id != v.id, Subscription.user_id.notin_( g.db.query( UserBlock.user_id).filter_by(target_id=v.id).subquery())) follow_uids = g.db.query(Follow.user_id).options(lazyload('*')).filter( Follow.target_id == v.id, Follow.get_notifs == True, Follow.user_id != v.id, Follow.user_id.notin_( g.db.query( UserBlock.user_id).filter_by(target_id=v.id).subquery()), Follow.user_id.notin_( g.db.query(UserBlock.target_id).filter_by( user_id=v.id).subquery())).join(Follow.target).filter( User.is_private == False, User.is_nofollow == False, ) if not new_post.is_public: contribs = g.db.query(ContributorRelationship).filter_by( board_id=new_post.board_id, is_active=True).subquery() mods = g.db.query(ModRelationship).filter_by( board_id=new_post.board_id, accepted=True).subquery() board_uids = board.uids.join( contribs, contribs.c.user_id == Subscription.user_id, isouter=True).join(mods, mods.c.user_id == Subscription.user_id, isouter=True).filter( or_(mods.c.id != None, contribs.c.id != None)) follow_uids = follow_uids.join(contribs, contribs.c.user_id == Follow.user_id, isouter=True).join( mods, mods.c.user_id == Follow.user_id, isouter=True).filter( or_(mods.c.id != None, contribs.c.id != None)) uids = list( set([x[0] for x in board_uids.all()] + [x[0] for x in follow_uids.all()])) for uid in uids: new_notif = Notification(user_id=uid, submission_id=new_post.id) g.db.add(new_notif) g.db.commit() return { "html": lambda: redirect(new_post.permalink), "api": lambda: jsonify(new_post.json) }
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","") with UserRenderer() as renderer: body_md=renderer.render(mistletoe.Document(body)) body_html=sanitize(body_md, linkgen=True) #Run safety filter ban=filter_comment_html(body_html) if ban: abort(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() elif parent_fullname.startswith("t3"): parent=db.query(Comment).filter_by(id=parent_id).first() #No commenting on deleted/removed things if parent.is_banned or parent.is_deleted: abort(403) #create comment c=Comment(author_id=v.id, body=body, body_html=body_html, parent_submission=parent_submission, parent_fullname=parent_fullname, ) db.add(c) 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("/u/(\w+)"), limit=3) for mention in mentions: username=mention["href"].split("/u/")[1] user=db.query(User).filter_by(username=username).first() if user: notify_users.add(user.id) for id in notify_users: n=Notification(comment_id=c.id, user_id=id) 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() return redirect(f"{c.post.permalink}#comment-{c.base36id}")
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) 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 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 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() 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})" 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() # 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 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, deleted_utc=int(time.time())) 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 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.is_deleted: abort(403) if c.board.has_ban(v): abort(403) body = request.form.get("body", "")[0:10000] 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: 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 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 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 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 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 g.db.query(Board).filter(Board.name.ilike(board_name)).first(): 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)