Beispiel #1
0
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
Beispiel #2
0
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
                                           )
                    }
    )
Beispiel #3
0
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
                                           )
                    }
    )
Beispiel #4
0
 def __init__(self, text: str):
     self.doc = mistletoe.Document(text)
Beispiel #5
0
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.")
Beispiel #6
0
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)
    }
Beispiel #7
0
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)
Beispiel #8
0
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
            }
Beispiel #9
0
def markdown_render(text, url, xref, toc):
    with DocRenderer(url, xref, toc) as renderer:
        return renderer.render(mistletoe.Document(text))
Beispiel #10
0
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)
Beispiel #11
0
def markdown_headers(text, url, xref):
    with DocPreRenderer(url, xref) as renderer:
        renderer.render(mistletoe.Document(text))
        return renderer._toc
Beispiel #12
0
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
    }
Beispiel #13
0
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)
Beispiel #14
0
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
Beispiel #15
0
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
Beispiel #16
0
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")
Beispiel #17
0
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)
Beispiel #18
0
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})
Beispiel #19
0
 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
Beispiel #20
0
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)
Beispiel #21
0
 def __init__(self, path: Path):
     self.doc = mistletoe.Document(path.read_text())
Beispiel #22
0
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)
Beispiel #23
0
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
    }
Beispiel #24
0
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)
Beispiel #25
0
 def md_to_latex(value):
     with LaTeXRenderer() as renderer:
         return renderer.render_inner(mistletoe.Document(value))