Example #1
0
def edit_comment(cid, v):

    c = get_comment(cid)

    if not c.author_id == v.id:
        abort(403)

    if c.is_banned or c.is_deleted:
        abort(403)

    if c.board.has_ban(v):
        abort(403)

    body = request.form.get("body", "")[0:10000]
    with CustomRenderer(post_id=c.post.base36id) as renderer:
        body_md = renderer.render(mistletoe.Document(body))
    body_html = sanitize(body_md, linkgen=True)

    #Run safety filter
    bans = filter_comment_html(body_html)

    if bans:
        return {
            'html':
            lambda: render_template("comment_failed.html",
                                    action=f"/edit_comment/{c.base36id}",
                                    badlinks=[x.domain for x in bans],
                                    body=body,
                                    v=v),
            'api':
            lambda: {
                'error': f'A blacklist domain was used.'
            }
        }

    c.body = body
    c.body_html = body_html
    c.edited_utc = int(time.time())

    g.db.add(c)

    c.determine_offensive()

    path = request.form.get("current_page", "/")

    return redirect(f"{path}#comment-{c.base36id}")
Example #2
0
def api_comment(v):

    parent_submission = base36decode(request.form.get("submission"))
    parent_fullname = request.form.get("parent_fullname")

    #sanitize
    body = request.form.get("body", "")
    body_md = mistletoe.markdown(body)
    body_html = sanitize(body_md, linkgen=True)

    #check existing
    existing = db.query(Comment).filter_by(
        author_id=v.id,
        body=body,
        parent_fullname=parent_fullname,
        parent_submission=parent_submission).first()
    if existing:
        return redirect(existing.permalink)

    parent_id = int(parent_fullname.split("_")[1], 36)
    if parent_fullname.startswith("t2"):
        parent = db.query(Submission).filter_by(id=parent_id).first()
    elif parent_fullname.startswith("t3"):
        parent = db.query(Comment).filter_by(id=parent_id).first()

    if parent.is_banned:
        abort(403)

    c = Comment(author_id=v.id,
                body=body,
                body_html=body_html,
                parent_submission=parent_submission,
                parent_fullname=parent_fullname,
                parent_author_id=parent.author.id
                if parent.author.id != v.id else None)

    db.add(c)
    db.commit()

    vote = CommentVote(user_id=v.id, comment_id=c.id, vote_type=1)

    db.add(vote)
    db.commit()

    return redirect(f"{c.post.permalink}#comment-{c.base36id}")
Example #3
0
def edit_comment(v):

    comment_id = request.form.get("id")
    body = request.form.get("comment", "")
    body_md = mistletoe.markdown(body)
    body_html = sanitize(body_md, linkgen=True)

    c = db.query(Comment).filter_by(id=comment_id, author_id=v.id).first()

    if not c:
        abort(404)

    c.body = body
    c.body_html = body_html
    c.edited_timestamp = time.time()

    db.add(c)
    db.commit()
Example #4
0
def direct_message(args, guild, v):
    """Send someone a private message. Use Tab to reply to the most recent private message"""

    if len(args) < 3:
        send("Not enough arguments. Type `/help msg` for more information.")
        return

    user = get_user(args[1], graceful=True)
    if not user:
        send(f"No user named @{args[1]}.")
        return

    targets = SIDS.get(user.id, [])
    if not targets:
        send(f"@{user.username} is not online right now.")
        return

    text = " ".join(args[2:])

    text = preprocess(text)
    with CustomRenderer() as renderer:
        text = renderer.render(mistletoe.Document(text))
    text = sanitize(text, linkgen=True)

    t = now()

    data = {
        "avatar": v.profile_url,
        "username": v.username,
        "text": text,
        "time": t
    }

    for sid in targets:
        emit('msg-in', data, to=sid)

    data = {
        "avatar": v.profile_url,
        "username": user.username,
        "text": text,
        "time": t
    }
    for sid in SIDS.get(v.id, []):
        emit('msg-out', data, to=sid)
Example #5
0
def speak(text, user, guild, as_guild=False, event="speak", to=None):

    if isinstance(text, list):
        text = " ".join(text)

    text = preprocess(text)
    with CustomRenderer() as renderer:
        text = renderer.render(mistletoe.Document(text))
    text = sanitize(text, linkgen=True)

    to = to or guild.fullname

    ban = screen(text)
    if ban:
        speak_help(f"Unable to send message - banned domain {ban}")
        return

    if as_guild or event == "motd":
        data = {
            "avatar": guild.profile_url,
            "username": guild.name,
            "text": text,
            "room": guild.fullname,
            "guild": guild.name,
            "time": now(),
            'userlink': guild.permalink
        }
        emit("motd", data, to=to)
    else:
        data = {
            "avatar": user.profile_url,
            "username": user.username,
            "text": text,
            "room": guild.fullname,
            "guild": guild.name,
            "time": now(),
            "userlink": user.permalink
        }

        if request.headers.get("X-User-Type", "").lower() == "bot":
            emit("bot", data, to=to)
        else:
            emit(event, data, to=to)
        return
Example #6
0
def speak_as_gm(args, guild, v):
    """Distinguish your message with a Guildmaster's crown. (Must be Guildmaster.)"""
    text = " ".join(args[1:])
    if not text:
        return
    text = preprocess(text)
    with CustomRenderer() as renderer:
        text = renderer.render(mistletoe.Document(text))
    text = sanitize(text, linkgen=True)

    data = {
        "avatar": v.profile_url,
        "username": v.username,
        "text": text,
        "room": guild.fullname,
        "guild": guild.name,
        "time": now()
    }
    emit('gm', data, to=guild.fullname)
Example #7
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))

    if request.form.get("bio") != v.bio:
        updated = True
        bio = request.form.get("bio")
        v.bio = bio

        v.bio_html = sanitize(bio)

    if updated:
        db.add(v)
        db.commit()

        return render_template("settings_profile.html",
                               v=v,
                               msg="Your settings have been saved.")

    else:
        return render_template("settings_profile.html",
                               v=v,
                               error="You didn't change anything.")
Example #8
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.")

    x = urlparse(url)
    if not (x.scheme and x.netloc):
        return render_template("submit.html", v=v, error="Please enter a URL.")

    #sanitize title
    title = sanitize(title, linkgen=False)

    #check for duplicate
    dup = db.query(Submission).filter_by(title=title, author_id=v.id,
                                         url=url).first()

    if dup:
        return redirect(dup.permalink)

    #now make new post

    new_post = Submission(title=title, url=url, author_id=v.id)

    #run through content filter
    x = filter_post(new_post)
    if x:
        return render_template("submit.html", v=v, error=x)

    db.add(new_post)

    db.commit()

    vote = Vote(user_id=v.id, vote_type=1, submission_id=new_post.id)
    db.add(vote)
    db.commit()

    return redirect(new_post.permalink)
Example #9
0
def api_comment(v):

    body = request.form.get("text")
    parent_fullname = request.form.get("parent_fullname")

    #sanitize
    body = request.form.get("body")
    body_html = mistletoe.markdown(body_md)
    body_html = sanitize(body_html, linkgen=True)

    #check existing
    existing = db.query(Comment).filter_by(
        author_id=v.id, body=body, parent_fullname=parent_fullname).first()
    if existing:
        return redirect(existing.permalink)

    c = Comment(author_id=v.id, body=body, body_html=body_html)

    db.add(c)
    db.commit()
Example #10
0
def speak_admin(args, guild, v):
    """Distinguish your message with an Administrator's shield. (Must be site administrator.)"""

    text = " ".join(args[1:])
    if not text:
        return
    text = preprocess(text)
    with CustomRenderer() as renderer:
        text = renderer.render(mistletoe.Document(text))
    text = sanitize(text, linkgen=True)

    data = {
        "avatar": v.profile_url,
        "username": v.username,
        "text": text,
        'guild': guild.name,
        'room': guild.fullname,
        "time": now()
    }
    emit('admin', data, to=guild.fullname)
Example #11
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)

    p.body = body
    p.body_html = body_html
    p.edited_utc = int(time.time())

    # offensive
    p.is_offensive = False
    for x in g.db.query(BadWord).all():
        if (p.body and x.check(p.body)) or x.check(p.title):
            p.is_offensive = True
            break


    # politics
    p.is_politics = False
    for x in g.db.query(PoliticsWord).all():
        if (p.body and x.check(p.body)) or x.check(p.title):
            p.is_politics = True
            break

    g.db.add(p)

    return redirect(p.permalink)
Example #12
0
def mod_add_rule(bid, board, v):
    # board description
    rule = request.form.get("rule1")
    rule2 = request.form.get("rule2")
    if not rule2:
        with CustomRenderer() as renderer:
            rule_md = renderer.render(mistletoe.Document(rule))
        rule_html = sanitize(rule_md, linkgen=True)

        new_rule = Rules(board_id=bid, rule_body=rule, rule_html=rule_html)
        g.db.add(new_rule)

    else:
        """
        im guessing here we should
        do a loop for
        adding multiple rules
        """
        pass

    return "", 204
Example #13
0
def wallop(args, guild, v):
    """Send a global broadcast. (Must be site administrator.)"""
    text = " ".join(args[1:])
    if not text:
        return
    text = preprocess(text)
    with CustomRenderer() as renderer:
        text = renderer.render(mistletoe.Document(text))
    text = sanitize(text, linkgen=True)

    data = {
        "avatar": v.profile_url,
        "username": v.username,
        "text": text,
        "time": now()
    }

    sent = []
    for uid in SIDS:
        for sid in SIDS[uid]:
            for roomid in rooms(sid=sid):
                if roomid.startswith('t4_') and roomid not in sent:
                    emit('wallop', data, to=roomid)
                    sent.append(roomid)
Example #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("nofollow", v.is_nofollow) != v.is_nofollow:
        updated = True
        v.is_nofollow = request.values.get("nofollow", None) == 'true'

    if request.values.get("bio") is not None:
        bio = request.values.get("bio")[0:256]

        if bio == v.bio:
            return render_template("settings_profile.html",
                                   v=v,
                                   error="You didn't change anything")

        v.bio = bio

        with CustomRenderer() as renderer:
            v.bio_html = renderer.render(mistletoe.Document(bio))
        v.bio_html = sanitize(v.bio_html, linkgen=True)
        g.db.add(v)
        return render_template("settings_profile.html",
                               v=v,
                               msg="Your bio has been updated.")

    x = request.values.get("title_id", None)
    if x:
        x = int(x)
        if x == 0:
            v.title_id = None
            updated = True
        elif x > 0:
            title = get_title(x)
            if bool(eval(title.qualification_expr)):
                v.title_id = title.id
                updated = True
            else:
                return jsonify({
                    "error":
                    f"You don't meet the requirements for title `{title.text}`."
                }), 403
        else:
            abort(400)

    if updated:
        g.db.add(v)

        return jsonify({"message": "Your settings have been updated."})

    else:
        return jsonify({"error": "You didn't change anything."}), 400
Example #15
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)
Example #16
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)
Example #17
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.")
Example #18
0
def submit_post(v):

    title = request.form.get("title", "")

    title = title.lstrip().rstrip()
    title = title.replace("\n", "")
    title = title.replace("\r", "")
    title = title.replace("\t", "")

    url = request.form.get("url", "")

    board = get_guild(request.form.get('board', 'general'), graceful=True)
    if not board:
        board = get_guild('general')

    if not title:
        return render_template("submit.html",
                               v=v,
                               error="Please enter a better title.",
                               title=title,
                               url=url,
                               body=request.form.get("body", ""),
                               b=board)

    # if len(title)<10:
    #     return render_template("submit.html",
    #                            v=v,
    #                            error="Please enter a better title.",
    #                            title=title,
    #                            url=url,
    #                            body=request.form.get("body",""),
    #                            b=board
    #                            )

    elif len(title) > 500:
        return render_template("submit.html",
                               v=v,
                               error="500 character limit for titles.",
                               title=title[0:500],
                               url=url,
                               body=request.form.get("body", ""),
                               b=board)

    parsed_url = urlparse(url)
    if not (parsed_url.scheme and parsed_url.netloc) and not request.form.get(
            "body") and not request.files.get("file", None):
        return render_template("submit.html",
                               v=v,
                               error="Please enter a URL or some text.",
                               title=title,
                               url=url,
                               body=request.form.get("body", ""),
                               b=board)
    #sanitize title
    title = bleach.clean(title)

    #Force https for submitted urls
    if request.form.get("url"):
        new_url = ParseResult(scheme="https",
                              netloc=parsed_url.netloc,
                              path=parsed_url.path,
                              params=parsed_url.params,
                              query=parsed_url.query,
                              fragment=parsed_url.fragment)
        url = urlunparse(new_url)
    else:
        url = ""

    body = request.form.get("body", "")
    #check for duplicate
    dup = g.db.query(Submission).join(Submission.submission_aux).filter(
        Submission.author_id == v.id, Submission.is_deleted == False,
        Submission.board_id == board.id, SubmissionAux.title == title,
        SubmissionAux.url == url, SubmissionAux.body == body).first()

    if dup:
        return redirect(dup.permalink)

    #check for domain specific rules

    parsed_url = urlparse(url)

    domain = parsed_url.netloc

    # check ban status
    domain_obj = get_domain(domain)
    if domain_obj:
        if not domain_obj.can_submit:
            return render_template("submit.html",
                                   v=v,
                                   error=BAN_REASONS[domain_obj.reason],
                                   title=title,
                                   url=url,
                                   body=request.form.get("body", ""),
                                   b=get_guild(request.form.get(
                                       "board", "general"),
                                               graceful=True))

        #check for embeds
        if domain_obj.embed_function:
            try:
                embed = eval(domain_obj.embed_function)(url)
            except:
                embed = ""
        else:
            embed = ""
    else:
        embed = ""

    #board
    board_name = request.form.get("board", "general")
    board_name = board_name.lstrip("+")
    board_name = board_name.rstrip()

    board = get_guild(board_name, graceful=True)

    if not board:
        board = get_guild('general')

    if board.is_banned:
        return render_template("submit.html",
                               v=v,
                               error=f"+{board.name} has been demolished.",
                               title=title,
                               url=url,
                               body=request.form.get("body", ""),
                               b=get_guild("general", graceful=True)), 403

    if board.has_ban(v):
        return render_template("submit.html",
                               v=v,
                               error=f"You are exiled from +{board.name}.",
                               title=title,
                               url=url,
                               body=request.form.get("body", ""),
                               b=get_guild("general")), 403

    if (board.restricted_posting
            or board.is_private) and not (board.can_submit(v)):
        return render_template(
            "submit.html",
            v=v,
            error=f"You are not an approved contributor for +{board.name}.",
            title=title,
            url=url,
            body=request.form.get("body", ""),
            b=get_guild(request.form.get("board", "general"), graceful=True))

    #similarity check
    now = int(time.time())
    cutoff = now - 60 * 60 * 24
    similar_posts = g.db.query(Submission).options(lazyload('*')).join(
        Submission.submission_aux).filter(
            Submission.author_id == v.id,
            SubmissionAux.title.op('<->')(title) <
            app.config["SPAM_SIMILARITY_THRESHOLD"],
            Submission.created_utc > cutoff).all()

    if url:
        similar_urls = g.db.query(Submission).options(lazyload('*')).join(
            Submission.submission_aux).filter(
                Submission.author_id == v.id,
                SubmissionAux.url.op('<->')(url) <
                app.config["SPAM_URL_SIMILARITY_THRESHOLD"],
                Submission.created_utc > cutoff).all()
    else:
        similar_urls = []

    threshold = app.config["SPAM_SIMILAR_COUNT_THRESHOLD"]
    if v.age >= (60 * 60 * 24 * 30):
        threshold *= 4
    elif v.age >= (60 * 60 * 24 * 7):
        threshold *= 3
    elif v.age >= (60 * 60 * 24):
        threshold *= 2

    if max(len(similar_urls), len(similar_posts)) >= threshold:

        text = "Your Ruqqus account has been suspended for 1 day for the following reason:\n\n> Too much spam!"
        send_notification(v, text)

        v.ban(reason="Spamming.", include_alts=True, days=1)

        for post in similar_posts + similar_urls:
            post.is_banned = True
            post.ban_reason = "Automatic spam removal. This happened because the post's creator submitted too much similar content too quickly."
            g.db.add(post)

        g.db.commit()
        return redirect("/notifications")

    #catch too-long body
    if len(str(body)) > 10000:

        return render_template("submit.html",
                               v=v,
                               error="10000 character limit for text body",
                               title=title,
                               text=str(body)[0:10000],
                               url=url,
                               b=get_guild(request.form.get(
                                   "board", "general"),
                                           graceful=True)), 400

    if len(url) > 2048:

        return render_template("submit.html",
                               v=v,
                               error="URLs cannot be over 2048 characters",
                               title=title,
                               text=body[0:2000],
                               b=get_guild(request.form.get(
                                   "board", "general"),
                                           graceful=True)), 400

    #render text

    with CustomRenderer() as renderer:
        body_md = renderer.render(mistletoe.Document(body))
    body_html = sanitize(body_md, linkgen=True)

    ##check spam
    soup = BeautifulSoup(body_html, features="html.parser")
    links = [x['href'] for x in soup.find_all('a') if x.get('href')]

    if url:
        links = [url] + links

    for link in links:
        parse_link = urlparse(link)
        check_url = ParseResult(scheme="https",
                                netloc=parse_link.netloc,
                                path=parse_link.path,
                                params=parse_link.params,
                                query=parse_link.query,
                                fragment='')
        check_url = urlunparse(check_url)

        badlink = g.db.query(BadLink).filter(
            literal(check_url).contains(BadLink.link)).first()
        if badlink:
            if badlink.autoban:
                text = "Your Ruqqus account has been suspended for 1 day for the following reason:\n\n> Too much spam!"
                send_notification(v, text)
                v.ban(days=1, reason="spam")

                return redirect('/notifications')
            else:
                return render_template(
                    "submit.html",
                    v=v,
                    error=
                    f"The link `{badlink.link}` is not allowed. Reason: {badlink.reason}",
                    title=title,
                    text=body[0:2000],
                    b=get_guild(request.form.get("board", "general"),
                                graceful=True)), 400

    #check for embeddable video
    domain = parsed_url.netloc

    if url:
        repost = g.db.query(Submission).join(Submission.submission_aux).filter(
            SubmissionAux.url.ilike(url), Submission.board_id == board.id,
            Submission.is_deleted == False,
            Submission.is_banned == False).order_by(
                Submission.id.asc()).first()
    else:
        repost = None

    if request.files.get('file') and not v.can_submit_image:
        abort(403)

    #offensive
    is_offensive = False
    for x in g.db.query(BadWord).all():
        if (body and x.check(body)) or x.check(title):
            is_offensive = True
            break

    new_post = Submission(author_id=v.id,
                          domain_ref=domain_obj.id if domain_obj else None,
                          board_id=board.id,
                          original_board_id=board.id,
                          over_18=(bool(request.form.get("over_18", ""))
                                   or board.over_18),
                          post_public=not board.is_private,
                          repost_id=repost.id if repost else None,
                          is_offensive=is_offensive)

    g.db.add(new_post)
    g.db.flush()

    new_post_aux = SubmissionAux(id=new_post.id,
                                 url=url,
                                 body=body,
                                 body_html=body_html,
                                 embed_url=embed,
                                 title=title)
    g.db.add(new_post_aux)
    g.db.flush()

    vote = Vote(user_id=v.id, vote_type=1, submission_id=new_post.id)
    g.db.add(vote)
    g.db.flush()

    g.db.commit()
    g.db.refresh(new_post)

    #check for uploaded image
    if request.files.get('file'):

        file = request.files['file']

        name = f'post/{new_post.base36id}/{secrets.token_urlsafe(8)}'
        upload_file(name, file)

        #thumb_name=f'posts/{new_post.base36id}/thumb.png'
        #upload_file(name, file, resize=(375,227))

        #update post data
        new_post.url = f'https://{BUCKET}/{name}'
        new_post.is_image = True
        new_post.domain_ref = 1  #id of i.ruqqus.com domain
        g.db.add(new_post)
        g.db.commit()

    #spin off thumbnail generation and csam detection as  new threads
    if new_post.url or request.files.get('file'):
        new_thread = threading.Thread(target=thumbnail_thread,
                                      args=(new_post.base36id, ))
        new_thread.start()
        csam_thread = threading.Thread(target=check_csam, args=(new_post, ))
        csam_thread.start()

    #expire the relevant caches: front page new, board new
    #cache.delete_memoized(frontlist, sort="new")
    g.db.commit()
    cache.delete_memoized(Board.idlist, board, sort="new")

    #print(f"Content Event: @{new_post.author.username} post {new_post.base36id}")

    return {
        "html": lambda: redirect(new_post.permalink),
        "api": lambda: jsonify(new_post.json)
    }
Example #19
0
def api_comment(v):

    parent_submission = base36decode(request.form.get("submission"))
    parent_fullname = request.form.get("parent_fullname")
    parent_post = get_post(request.form.get("submission"))

    #process and sanitize
    body = request.form.get("body", "")[0:10000]
    body = body.lstrip().rstrip()

    with CustomRenderer(post_id=request.form.get("submission")) as renderer:
        body_md = renderer.render(mistletoe.Document(body))
    body_html = sanitize(body_md, linkgen=True)

    #Run safety filter
    bans = filter_comment_html(body_html)

    if bans:
        ban = bans[0]
        reason = f"Remove the {ban.domain} link from your comment and try again."
        if ban.reason:
            reason += f" {ban.reason_text}"
        return jsonify({"error": reason}), 401

    #get parent item info
    parent_id = parent_fullname.split("_")[1]
    if parent_fullname.startswith("t2"):
        parent = parent_post
        parent_comment_id = None
        level = 1
    elif parent_fullname.startswith("t3"):
        parent = get_comment(parent_id, v=v)
        parent_comment_id = parent.id
        level = parent.level + 1

    #check existing
    existing = g.db.query(Comment).join(CommentAux).filter(
        Comment.author_id == v.id, Comment.is_deleted == False,
        Comment.parent_comment_id == parent_comment_id,
        Comment.parent_submission == parent_submission,
        CommentAux.body == body).options(contains_eager(
            Comment.comment_aux)).first()
    if existing:
        return jsonify(
            {"error":
             f"You already made that comment: {existing.permalink}"}), 409

    #No commenting on deleted/removed things
    if parent.is_banned or parent.is_deleted:
        return jsonify(
            {"error":
             "You can't comment on things that have been deleted."}), 403

    if parent.author.any_block_exists(v):
        return jsonify({
            "error":
            "You can't reply to users who have blocked you, or users you have blocked."
        }), 403

    #check for archive and ban state
    post = get_post(request.form.get("submission"))
    if post.is_archived or not post.board.can_comment(v):
        return jsonify({"error": "You can't comment on this."}), 403

    #check spam - this is stupid slow for now
    #similar_comments=g.db.query(Comment).filter(Comment.author_id==v.id, CommentAux.body.op('<->')(body)<0.5).options(contains_eager(Comment.comment_aux)).all()
    #print(similar_comments)

    for x in g.db.query(BadWord).all():
        if x.check(body):
            is_offensive = True
            break
        else:
            is_offensive = False

    #check badlinks
    soup = BeautifulSoup(body_html, features="html.parser")
    links = [x['href'] for x in soup.find_all('a') if x.get('href')]

    for link in links:
        parse_link = urlparse(link)
        check_url = ParseResult(scheme="https",
                                netloc=parse_link.netloc,
                                path=parse_link.path,
                                params=parse_link.params,
                                query=parse_link.query,
                                fragment='')
        check_url = urlunparse(check_url)

        badlink = g.db.query(BadLink).filter(
            literal(check_url).contains(BadLink.link)).first()

        if badlink:
            return jsonify({
                "error":
                f"Remove the following link and try again: `{check_url}`. Reason: {badlink.reason_text}"
            }), 403

    #create comment
    c = Comment(author_id=v.id,
                parent_submission=parent_submission,
                parent_fullname=parent_fullname,
                parent_comment_id=parent_comment_id,
                level=level,
                over_18=post.over_18,
                is_nsfl=post.is_nsfl,
                is_op=(v.id == post.author_id),
                is_offensive=is_offensive,
                original_board_id=parent_post.board_id)

    g.db.add(c)
    g.db.flush()

    c_aux = CommentAux(id=c.id, body_html=body_html, body=body)
    g.db.add(c_aux)
    g.db.flush()

    notify_users = set()

    #queue up notification for parent author
    if parent.author.id != v.id:
        notify_users.add(parent.author.id)

    #queue up notifications for username mentions
    soup = BeautifulSoup(body_html, features="html.parser")
    mentions = soup.find_all("a", href=re.compile("^/@(\w+)"), limit=3)
    for mention in mentions:
        username = mention["href"].split("@")[1]

        user = g.db.query(User).filter_by(username=username).first()

        if user:
            if v.any_block_exists(user):
                continue
            if user.id != v.id:
                notify_users.add(user.id)

    for x in notify_users:
        n = Notification(comment_id=c.id, user_id=x)
        g.db.add(n)

    #create auto upvote
    vote = CommentVote(user_id=v.id, comment_id=c.id, vote_type=1)

    g.db.add(vote)

    g.db.commit()

    #print(f"Content Event: @{v.username} comment {c.base36id}")

    return jsonify({
        "html":
        render_template("comments.html",
                        v=v,
                        comments=[c],
                        render_replies=False,
                        is_allowed_to_comment=True)
    })
Example #20
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
                                           )
                    }
    )
Example #21
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,
                               v=v
                               ), 422

    #check existing
    existing=g.db.query(Comment).filter_by(author_id=v.id,
                                         body=body,
                                         parent_fullname=parent_fullname,
                                         parent_submission=parent_submission
                                         ).first()
    if existing:
        return redirect(existing.permalink)

    #get parent item info
    parent_id=int(parent_fullname.split("_")[1], 36)
    if parent_fullname.startswith("t2"):
        parent=g.db.query(Submission).filter_by(id=parent_id).first()
        parent_comment_id=None
        level=1
    elif parent_fullname.startswith("t3"):
        parent=g.db.query(Comment).filter_by(id=parent_id).first()
        parent_comment_id=parent.id
        level=parent.level+1

    #No commenting on deleted/removed things
    if parent.is_banned or parent.is_deleted:
        abort(403)

    #check for ban state
    post = get_post(request.form.get("submission"))
    if post.is_archived or not post.board.can_comment(v):
        abort(403)

        
    #create comment
    c=Comment(author_id=v.id,
              body=body,
              body_html=body_html,
              parent_submission=parent_submission,
              parent_fullname=parent_fullname,
              parent_comment_id=parent_comment_id,
              level=level,
              author_name=v.username,
              over_18=post.over_18,
              is_nsfl=post.is_nsfl,
              is_op=(v.id==post.author_id)
              )

    c.determine_offensive()
    g.db.add(c)
    
    g.db.commit()
    g.db.begin()   


    notify_users=set()

    #queue up notification for parent author
    if parent.author.id != v.id:
        notify_users.add(parent.author.id)

    #queue up notifications for username mentions
    soup=BeautifulSoup(c.body_html, features="html.parser")
    mentions=soup.find_all("a", href=re.compile("^/@(\w+)"), limit=3)
    for mention in mentions:
        username=mention["href"].split("@")[1]
        user=g.db.query(User).filter_by(username=username).first()
        if user:
            notify_users.add(user.id)


    for x in notify_users:
        n=Notification(comment_id=c.id,
                       user_id=x)
        g.db.add(n)
    


    #create auto upvote
    vote=CommentVote(user_id=v.id,
                     comment_id=c.id,
                     vote_type=1
                     )

    g.db.add(vote)
    

    #print(f"Content Event: @{v.username} comment {c.base36id}")

    return redirect(f"{c.permalink}?context=1")
Example #22
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:

        return {
            "html":
            lambda:
            (render_template("submit.html",
                             v=v,
                             error=f"Please enter a Guild to submit to.",
                             title=title,
                             url=url,
                             body=request.form.get("body", ""),
                             b=None), 403),
            "api":
            lambda: (jsonify(
                {"error": f"403 Forbidden - +{board.name} has been banned."}))
        }

    if board.is_banned:

        return {
            "html":
            lambda: (render_template("submit.html",
                                     v=v,
                                     error=f"+{board.name} has been banned.",
                                     title=title,
                                     url=url,
                                     body=request.form.get("body", ""),
                                     b=None), 403),
            "api":
            lambda: (jsonify(
                {"error": f"403 Forbidden - +{board.name} has been banned."}))
        }

    if board.has_ban(v):
        return {
            "html":
            lambda:
            (render_template("submit.html",
                             v=v,
                             error=f"You are exiled from +{board.name}.",
                             title=title,
                             url=url,
                             body=request.form.get("body", ""),
                             b=None), 403),
            "api":
            lambda: (jsonify({
                "error":
                f"403 Not Authorized - You are exiled from +{board.name}"
            }), 403)
        }

    if (board.restricted_posting
            or board.is_private) and not (board.can_submit(v)):
        return {
            "html":
            lambda: (render_template(
                "submit.html",
                v=v,
                error=
                f"You are not an approved contributor for +{board.name}.",
                title=title,
                url=url,
                body=request.form.get("body", ""),
                b=None), 403),
            "api":
            lambda: (jsonify({
                "error":
                f"403 Not Authorized - You are not an approved contributor for +{board.name}"
            }), 403)
        }

    if board.disallowbots and request.headers.get("X-User-Type") == "Bot":
        return {
            "api":
            lambda: (jsonify({
                "error":
                f"403 Not Authorized - +{board.name} disallows bots from posting and commenting!"
            }), 403)
        }

    # similarity check
    now = int(time.time())
    cutoff = now - 60 * 60 * 24

    similar_posts = g.db.query(Submission).options(lazyload('*')).join(
        Submission.submission_aux
    ).filter(
        #or_(
        #    and_(
        Submission.author_id == v.id,
        SubmissionAux.title.op('<->')(title) <
        app.config["SPAM_SIMILARITY_THRESHOLD"],
        Submission.created_utc > cutoff
        #    ),
        #    and_(
        #        SubmissionAux.title.op('<->')(title) < app.config["SPAM_SIMILARITY_THRESHOLD"]/2,
        #        Submission.created_utc > cutoff
        #    )
        #)
    ).all()

    if url:
        similar_urls = g.db.query(Submission).options(lazyload('*')).join(
            Submission.submission_aux
        ).filter(
            #or_(
            #    and_(
            Submission.author_id == v.id,
            SubmissionAux.url.op('<->')(url) <
            app.config["SPAM_URL_SIMILARITY_THRESHOLD"],
            Submission.created_utc > cutoff
            #    ),
            #    and_(
            #        SubmissionAux.url.op('<->')(url) < app.config["SPAM_URL_SIMILARITY_THRESHOLD"]/2,
            #        Submission.created_utc > cutoff
            #    )
            #)
        ).all()
    else:
        similar_urls = []

    threshold = app.config["SPAM_SIMILAR_COUNT_THRESHOLD"]
    if v.age >= (60 * 60 * 24 * 7):
        threshold *= 3
    elif v.age >= (60 * 60 * 24):
        threshold *= 2

    if max(len(similar_urls), len(similar_posts)) >= threshold:

        text = "Your Ruqqus account has been suspended for 1 day for the following reason:\n\n> Too much spam!"
        send_notification(v, text)

        v.ban(reason="Spamming.", days=1)

        for alt in v.alts:
            if not alt.is_suspended:
                alt.ban(reason="Spamming.", days=1)

        for post in similar_posts + similar_urls:
            post.is_banned = True
            post.is_pinned = False
            post.ban_reason = "Automatic spam removal. This happened because the post's creator submitted too much similar content too quickly."
            g.db.add(post)
            ma = ModAction(user_id=1,
                           target_submission_id=post.id,
                           kind="ban_post",
                           board_id=post.board_id,
                           note="spam")
            g.db.add(ma)
        g.db.commit()
        return redirect("/notifications")

    # catch too-long body
    if len(str(body)) > 10000:

        return {
            "html":
            lambda:
            (render_template("submit.html",
                             v=v,
                             error="10000 character limit for text body.",
                             title=title,
                             url=url,
                             body=request.form.get("body", ""),
                             b=board), 400),
            "api":
            lambda: ({
                "error": "10000 character limit for text body."
            }, 400)
        }

    if len(url) > 2048:

        return {
            "html":
            lambda: (render_template("submit.html",
                                     v=v,
                                     error="2048 character limit for URLs.",
                                     title=title,
                                     url=url,
                                     body=request.form.get("body", ""),
                                     b=board), 400),
            "api":
            lambda: ({
                "error": "2048 character limit for URLs."
            }, 400)
        }

    # render text

    body = preprocess(body)

    with CustomRenderer() as renderer:
        body_md = renderer.render(mistletoe.Document(body))
    body_html = sanitize(body_md, linkgen=True)

    # Run safety filter
    bans = filter_comment_html(body_html)
    if bans:
        ban = bans[0]
        reason = f"Remove the {ban.domain} link from your post and try again."
        if ban.reason:
            reason += f" {ban.reason_text}"

        #auto ban for digitally malicious content
        if any([x.reason == 4 for x in bans]):
            v.ban(days=30,
                  reason="Digitally malicious content is not allowed.")
            abort(403)

        return {
            "html":
            lambda: (render_template("submit.html",
                                     v=v,
                                     error=reason,
                                     title=title,
                                     url=url,
                                     body=request.form.get("body", ""),
                                     b=board), 403),
            "api":
            lambda: ({
                "error": reason
            }, 403)
        }

    # check spam
    soup = BeautifulSoup(body_html, features="html.parser")
    links = [x['href'] for x in soup.find_all('a') if x.get('href')]

    if url:
        links = [url] + links

    for link in links:
        parse_link = urlparse(link)
        check_url = ParseResult(scheme="https",
                                netloc=parse_link.netloc,
                                path=parse_link.path,
                                params=parse_link.params,
                                query=parse_link.query,
                                fragment='')
        check_url = urlunparse(check_url)

        badlink = g.db.query(BadLink).filter(
            literal(check_url).contains(BadLink.link)).first()
        if badlink:
            if badlink.autoban:
                text = "Your Ruqqus account has been suspended for 1 day for the following reason:\n\n> Too much spam!"
                send_notification(v, text)
                v.ban(days=1, reason="spam")

                return redirect('/notifications')
            else:

                return {
                    "html":
                    lambda: (render_template(
                        "submit.html",
                        v=v,
                        error=
                        f"The link `{badlink.link}` is not allowed. Reason: {badlink.reason}.",
                        title=title,
                        url=url,
                        body=request.form.get("body", ""),
                        b=board), 400),
                    "api":
                    lambda: ({
                        "error":
                        f"The link `{badlink.link}` is not allowed. Reason: {badlink.reason}"
                    }, 400)
                }

    # check for embeddable video
    domain = parsed_url.netloc

    if url:
        repost = g.db.query(Submission).join(Submission.submission_aux).filter(
            SubmissionAux.url.ilike(url), Submission.board_id == board.id,
            Submission.deleted_utc == 0,
            Submission.is_banned == False).order_by(
                Submission.id.asc()).first()
    else:
        repost = None

    if repost and request.values.get("no_repost"):
        return redirect(repost.permalink)

    if request.files.get('file') and not v.can_submit_image:
        abort(403)

    # offensive
    is_offensive = False
    for x in g.db.query(BadWord).all():
        if (body and x.check(body)) or x.check(title):
            is_offensive = True
            break

    new_post = Submission(author_id=v.id,
                          domain_ref=domain_obj.id if domain_obj else None,
                          board_id=board.id,
                          original_board_id=board.id,
                          over_18=(bool(request.form.get("over_18", ""))
                                   or board.over_18),
                          post_public=not board.is_private,
                          repost_id=repost.id if repost else None,
                          is_offensive=is_offensive,
                          app_id=v.client.application.id if v.client else None,
                          creation_region=request.headers.get("cf-ipcountry"),
                          is_bot=request.headers.get("X-User-Type",
                                                     "").lower() == "bot")

    g.db.add(new_post)
    g.db.flush()

    new_post_aux = SubmissionAux(id=new_post.id,
                                 url=url,
                                 body=body,
                                 body_html=body_html,
                                 embed_url=embed,
                                 title=title)
    g.db.add(new_post_aux)
    g.db.flush()

    vote = Vote(user_id=v.id, vote_type=1, submission_id=new_post.id)
    g.db.add(vote)
    g.db.flush()

    g.db.refresh(new_post)

    # check for uploaded image
    if request.files.get('file'):

        #check file size
        if request.content_length > 16 * 1024 * 1024 and not v.has_premium:
            g.db.rollback()
            abort(413)

        file = request.files['file']
        if not file.content_type.startswith('image/'):
            return {
                "html":
                lambda: (render_template("submit.html",
                                         v=v,
                                         error=f"Image files only.",
                                         title=title,
                                         body=request.form.get("body", ""),
                                         b=board), 400),
                "api":
                lambda: ({
                    "error": f"Image files only"
                }, 400)
            }

        name = f'post/{new_post.base36id}/{secrets.token_urlsafe(8)}'
        upload_file(name, file)

        # thumb_name=f'posts/{new_post.base36id}/thumb.png'
        #upload_file(name, file, resize=(375,227))

        # update post data
        new_post.url = f'https://{BUCKET}/{name}'
        new_post.is_image = True
        new_post.domain_ref = 1  # id of i.ruqqus.com domain
        g.db.add(new_post)
        g.db.add(new_post.submission_aux)
        g.db.commit()

        #csam detection
        def del_function():
            db = db_session()
            delete_file(name)
            new_post.is_banned = True
            db.add(new_post)
            db.commit()
            ma = ModAction(kind="ban_post",
                           user_id=1,
                           note="banned image",
                           target_submission_id=new_post.id)
            db.add(ma)
            db.commit()
            db.close()

        csam_thread = threading.Thread(target=check_csam_url,
                                       args=(f"https://{BUCKET}/{name}", v,
                                             del_function))
        csam_thread.start()

    g.db.commit()

    # spin off thumbnail generation and csam detection as  new threads
    if (new_post.url or request.files.get('file')) and (
            v.is_activated or request.headers.get('cf-ipcountry') != "T1"):
        new_thread = gevent.spawn(thumbnail_thread, new_post.base36id)

    # expire the relevant caches: front page new, board new
    cache.delete_memoized(frontlist)
    g.db.commit()
    cache.delete_memoized(Board.idlist, board, sort="new")

    # queue up notifications for username mentions
    notify_users = set()

    soup = BeautifulSoup(body_html, features="html.parser")
    for mention in soup.find_all("a", href=re.compile("^/@(\w+)"), limit=3):
        username = mention["href"].split("@")[1]
        user = g.db.query(User).filter_by(username=username).first()
        if user and not v.any_block_exists(user) and user.id != v.id:
            notify_users.add(user.id)

    for x in notify_users:
        send_notification(
            x,
            f"@{v.username} has mentioned you: https://ruqqus.com{new_post.permalink}"
        )

    # print(f"Content Event: @{new_post.author.username} post
    # {new_post.base36id}")

    #Bell notifs

    board_uids = g.db.query(Subscription.user_id).options(
        lazyload('*')).filter(
            Subscription.board_id == new_post.board_id,
            Subscription.is_active == True, Subscription.get_notifs == True,
            Subscription.user_id != v.id,
            Subscription.user_id.notin_(
                g.db.query(
                    UserBlock.user_id).filter_by(target_id=v.id).subquery()))

    follow_uids = g.db.query(Follow.user_id).options(lazyload('*')).filter(
        Follow.target_id == v.id, Follow.get_notifs == True,
        Follow.user_id != v.id,
        Follow.user_id.notin_(
            g.db.query(
                UserBlock.user_id).filter_by(target_id=v.id).subquery()),
        Follow.user_id.notin_(
            g.db.query(UserBlock.target_id).filter_by(
                user_id=v.id).subquery())).join(Follow.target).filter(
                    User.is_private == False,
                    User.is_nofollow == False,
                )

    if not new_post.is_public:

        contribs = g.db.query(ContributorRelationship).filter_by(
            board_id=new_post.board_id, is_active=True).subquery()
        mods = g.db.query(ModRelationship).filter_by(
            board_id=new_post.board_id, accepted=True).subquery()

        board_uids = board.uids.join(
            contribs, contribs.c.user_id == Subscription.user_id,
            isouter=True).join(mods,
                               mods.c.user_id == Subscription.user_id,
                               isouter=True).filter(
                                   or_(mods.c.id != None,
                                       contribs.c.id != None))

        follow_uids = follow_uids.join(contribs,
                                       contribs.c.user_id == Follow.user_id,
                                       isouter=True).join(
                                           mods,
                                           mods.c.user_id == Follow.user_id,
                                           isouter=True).filter(
                                               or_(mods.c.id != None,
                                                   contribs.c.id != None))

    uids = list(
        set([x[0]
             for x in board_uids.all()] + [x[0] for x in follow_uids.all()]))

    for uid in uids:
        new_notif = Notification(user_id=uid, submission_id=new_post.id)
        g.db.add(new_notif)
    g.db.commit()

    return {
        "html": lambda: redirect(new_post.permalink),
        "api": lambda: jsonify(new_post.json)
    }
Example #23
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","")
    with UserRenderer() as renderer:
        body_md=renderer.render(mistletoe.Document(body))
    body_html=sanitize(body_md, linkgen=True)

    #Run safety filter
    ban=filter_comment_html(body_html)

    if ban:
        abort(422)

    #check existing
    existing=db.query(Comment).filter_by(author_id=v.id,
                                         body=body,
                                         parent_fullname=parent_fullname,
                                         parent_submission=parent_submission
                                         ).first()
    if existing:
        return redirect(existing.permalink)

    #get parent item info
    parent_id=int(parent_fullname.split("_")[1], 36)
    if parent_fullname.startswith("t2"):
        parent=db.query(Submission).filter_by(id=parent_id).first()
    elif parent_fullname.startswith("t3"):
        parent=db.query(Comment).filter_by(id=parent_id).first()

    #No commenting on deleted/removed things
    if parent.is_banned or parent.is_deleted:
        abort(403)

    #create comment
    c=Comment(author_id=v.id,
              body=body,
              body_html=body_html,
              parent_submission=parent_submission,
              parent_fullname=parent_fullname,
              )


        

    db.add(c)
    db.commit()

    notify_users=set()

    #queue up notification for parent author
    if parent.author.id != v.id:
        notify_users.add(parent.author.id)

    #queue up notifications for username mentions
    soup=BeautifulSoup(c.body_html, features="html.parser")
    mentions=soup.find_all("a", href=re.compile("/u/(\w+)"), limit=3)
    for mention in mentions:
        username=mention["href"].split("/u/")[1]
        user=db.query(User).filter_by(username=username).first()
        if user:
            notify_users.add(user.id)


    for id in notify_users:
        n=Notification(comment_id=c.id,
                       user_id=id)
        db.add(n)
    db.commit()
                           

    #create auto upvote
    vote=CommentVote(user_id=v.id,
                     comment_id=c.id,
                     vote_type=1
                     )

    db.add(vote)
    db.commit()

    return redirect(f"{c.post.permalink}#comment-{c.base36id}")
Example #24
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)

    if updated:
        g.db.add(v)

        return jsonify({"message": "Your settings have been updated."})

    else:
        return jsonify({"error": "You didn't change anything."}), 400
Example #25
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)
Example #26
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()

    if v.has_premium:
        if request.files.get("file"):
            file = request.files["file"]
            if not file.content_type.startswith('image/'):
                return jsonify({"error": "That wasn't an image!"}), 400

            name = f'comment/{c.base36id}/{secrets.token_urlsafe(8)}'
            upload_file(name, file)

            body = request.form.get(
                "body") + f"\n\n![](https://{BUCKET}/{name})"

            with CustomRenderer(post_id=parent_id) as renderer:
                body_md = renderer.render(mistletoe.Document(body))
            body_html = sanitize(body_md, linkgen=True)

            #csam detection
            def del_function():
                delete_file(name)
                c.is_banned = True
                g.db.add(c)
                g.db.commit()

            csam_thread = threading.Thread(target=check_csam_url,
                                           args=(f"https://{BUCKET}/{name}", v,
                                                 del_function))
            csam_thread.start()

    c_aux = CommentAux(id=c.id, body_html=body_html, body=body)
    g.db.add(c_aux)
    g.db.flush()

    notify_users = set()

    # queue up notification for parent author
    if parent.author.id != v.id:
        notify_users.add(parent.author.id)

    # queue up notifications for username mentions
    soup = BeautifulSoup(body_html, features="html.parser")
    mentions = soup.find_all("a", href=re.compile("^/@(\w+)"), limit=3)
    for mention in mentions:
        username = mention["href"].split("@")[1]

        user = g.db.query(User).filter_by(username=username).first()

        if user:
            if v.any_block_exists(user):
                continue
            if user.id != v.id:
                notify_users.add(user.id)

    for x in notify_users:
        n = Notification(comment_id=c.id, user_id=x)
        g.db.add(n)

    # create auto upvote
    vote = CommentVote(user_id=v.id, comment_id=c.id, vote_type=1)

    g.db.add(vote)

    c.post.score_activity = c.post.rank_activity
    g.db.add(c.post)

    g.db.commit()

    # print(f"Content Event: @{v.username} comment {c.base36id}")

    return {
        "html":
        lambda: jsonify({
            "html":
            render_template("comments.html",
                            v=v,
                            comments=[c],
                            render_replies=False,
                            is_allowed_to_comment=True)
        }),
        "api":
        lambda: c.json
    }
Example #27
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,
                deleted_utc=int(time.time()))
    g.db.add(c)
    g.db.flush()

    c_aux = CommentAux(id=c.id, body_html=c_html, body=comment_text)

    g.db.add(c_aux)
    g.db.commit()

    return render_template("/help/dmca.html",
                           msg="Your request has been saved.",
                           v=v)
Example #28
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.is_deleted:
        abort(403)

    if c.board.has_ban(v):
        abort(403)

    body = request.form.get("body", "")[0:10000]
    with CustomRenderer(post_id=c.post.base36id) as renderer:
        body_md = renderer.render(mistletoe.Document(body))
    body_html = sanitize(body_md, linkgen=True)

    # Run safety filter
    bans = filter_comment_html(body_html)

    if bans:
        return {
            'html':
            lambda: render_template("comment_failed.html",
                                    action=f"/edit_comment/{c.base36id}",
                                    badlinks=[x.domain for x in bans],
                                    body=body,
                                    v=v),
            'api':
            lambda: ({
                'error': f'A blacklisted domain was used.'
            }, 400)
        }

    for x in g.db.query(BadWord).all():
        if x.check(body):
            c.is_offensive = True
            break
        else:
            c.is_offensive = False

    # check badlinks
    soup = BeautifulSoup(body_html, features="html.parser")
    links = [x['href'] for x in soup.find_all('a') if x.get('href')]

    for link in links:
        parse_link = urlparse(link)
        check_url = ParseResult(scheme="https",
                                netloc=parse_link.netloc,
                                path=parse_link.path,
                                params=parse_link.params,
                                query=parse_link.query,
                                fragment='')
        check_url = urlunparse(check_url)

        badlink = g.db.query(BadLink).filter(
            literal(check_url).contains(BadLink.link)).first()

        if badlink:
            return jsonify({
                "error":
                f"Remove the following link and try again: `{check_url}`. Reason: {badlink.reason_text}"
            }), 403

    # check spam - this should hopefully be faster
    now = int(time.time())
    cutoff = now - 60 * 60 * 24

    similar_comments = g.db.query(Comment).options(lazyload('*')).join(
        Comment.comment_aux).filter(
            Comment.author_id == v.id,
            CommentAux.body.op('<->')(body) <
            app.config["SPAM_SIMILARITY_THRESHOLD"],
            Comment.created_utc > cutoff).options(
                contains_eager(Comment.comment_aux)).all()

    threshold = app.config["SPAM_SIMILAR_COUNT_THRESHOLD"]
    if v.age >= (60 * 60 * 24 * 30):
        threshold *= 4
    elif v.age >= (60 * 60 * 24 * 7):
        threshold *= 3
    elif v.age >= (60 * 60 * 24):
        threshold *= 2

    if len(similar_comments) > threshold:
        text = "Your Ruqqus account has been suspended for 1 day for the following reason:\n\n> Too much spam!"
        send_notification(v, text)

        v.ban(reason="Spamming.", include_alts=True, days=1)

        for comment in similar_comments:
            comment.is_banned = True
            comment.ban_reason = "Automatic spam removal. This happened because the post's creator submitted too much similar content too quickly."
            g.db.add(comment)

        g.db.commit()
        return jsonify({"error": "Too much spam!"}), 403

    c.body = body
    c.body_html = body_html
    c.edited_utc = int(time.time())

    g.db.add(c)

    g.db.commit()

    path = request.form.get("current_page", "/")

    return jsonify({"html": c.body_html})
Example #29
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)
Example #30
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 g.db.query(Board).filter(Board.name.ilike(board_name)).first():
        return render_template("make_board.html",
                               v=v,
                               error="That Guild already exists.",
                               description=description
                               )

    #check # recent boards made by user
    cutoff=int(time.time())-60*60*24
    recent=g.db.query(Board).filter(Board.creator_id==v.id, Board.created_utc >= cutoff).all()
    if len([x for x in recent])>=2:
        return render_template("message.html",
                               title="You need to wait a bit.",
                               message="You can only create up to 2 guilds per day. Try again later."
                               ), 429



    with CustomRenderer() as renderer:
        description_md=renderer.render(mistletoe.Document(description))
    description_html=sanitize(description_md, linkgen=True)

    #make the board

    new_board=Board(name=board_name,
                    description=description,
                    description_html=description_html,
                    over_18=bool(request.form.get("over_18","")),
                    creator_id=v.id
                    )

    g.db.add(new_board)

    g.db.commit()
    
    

    #add user as mod
    mod=ModRelationship(user_id=v.id,
                        board_id=new_board.id,
                        accepted=True)
    g.db.add(mod)

    #add subscription for user
    sub=Subscription(user_id=v.id,
                     board_id=new_board.id)
    g.db.add(sub)
    

    #clear cache
    cache.delete_memoized(guild_ids, sort="new")

    return redirect(new_board.permalink)