Kickass online communities, without censorship

Ruqqus is an open-source web app that lets you enjoy online communities focused around topics you love, without censorship and moderator abuse.

Make a Ruqqus

The gist

Share, vote, and discuss. All without censorship.

On Ruqqus, you're free to share and say anything protected by the First Amendment. No censorship. No shadowbanning. No bullshit.

Share

Share articles, photos, or videos found on the web in topic-specific communities called guilds.

Discuss

Comment on other people's posts and engage in boundless, free discussion without censorship.

Vote

Vote on content you like or dislike. The content with the most upvotes makes it to the top!

Open source

Fair And Transparent Algorithms

We like to keep things transparent. For this reason, our entire codebase is open-source and available on Github. View our algorithms right from your browser. No biased trending or sorting manipulation on Ruqqus.

View our code
from flask import *
from time import time, strftime, gmtime
from sqlalchemy import *

from ruqqus.helpers.base36 import *
from ruqqus.__main__ import Base, db

class Vote(Base):

    __tablename__="votes"

    id=Column(Integer, primary_key=True)
    user_id=Column(Integer, ForeignKey("users.id"))
    vote_type=Column(Integer)
    submission_id=Column(Integer, ForeignKey("submissions.id"))
    created_utc=Column(Integer, default=0)

    
    def __init__(self, *args, **kwargs):
        
        if "created_utc" not in kwargs:
            kwargs["created_utc"]=int(time())

        super().__init__(*args, **kwargs)
            

    def change_to(self, x):

        """
        1 - upvote
        0 - novote
        -1 - downvote
        """
        if x in ["-1","0","1"]:
            x=int(x)
        elif x not in [-1, 0, 1]:
            abort(400)

        self.vote_type=x
        self.created_utc=int(time())

        db.add(self)
        db.commit()

class CommentVote(Base):

    __tablename__="commentvotes"

    id=Column(Integer, primary_key=True)
    user_id=Column(Integer, ForeignKey("users.id"))
    vote_type=Column(Integer)
    comment_id=Column(Integer, ForeignKey("comments.id"))
    created_utc=Column(Integer, default=0)

    def __init__(self, *args, **kwargs):
        
        if "created_utc" not in kwargs:
            kwargs["created_utc"]=int(time())

        super().__init__(*args, **kwargs)
            

    def change_to(self, x):

        """
        1 - upvote
        0 - novote
        -1 - downvote
        """
        if x in ["-1","0","1"]:
            x=int(x)
        elif x not in [-1, 0, 1]:
            abort(400)

        self.vote_type=x
        self.created_utc=int(time())

        db.add(self)
        db.commit()
from flask import render_template, request, abort
import time
from sqlalchemy import *
from sqlalchemy.orm import relationship, deferred
import math
from urllib.parse import urlparse
import random
from os import environ
import requests
from .mix_ins import *
from ruqqus.helpers.base36 import *
from ruqqus.helpers.lazy import lazy
import ruqqus.helpers.aws as aws
from ruqqus.__main__ import Base, db, cache
from .votes import Vote
from .domains import Domain
from .flags import Flag

class Submission(Base, Stndrd, Age_times, Scores, Fuzzing):
 
    __tablename__="submissions"

    id = Column(BigInteger, primary_key=True)
    author_id = Column(BigInteger, ForeignKey("users.id"))
    title = Column(String(500), default=None)
    url = Column(String(500), default=None)
    edited_utc = Column(BigInteger, default=0)
    created_utc = Column(BigInteger, default=0)
    is_banned = Column(Boolean, default=False)
    is_deleted=Column(Boolean, default=False)
    distinguish_level=Column(Integer, default=0)
    created_str=Column(String(255), default=None)
    stickied=Column(Boolean, default=False)
    comments=relationship("Comment", lazy="dynamic", backref="submissions")
    body=Column(String(5000), default="")
    body_html=Column(String(6000), default="")
    embed_url=Column(String(256), default="")
    domain_ref=Column(Integer, ForeignKey("domains.id"))
    flags=relationship("Flag", lazy="dynamic", backref="submission")
    is_approved=Column(Integer, ForeignKey("users.id"), default=0)
    approved_utc=Column(Integer, default=0)
    board_id=Column(Integer, ForeignKey("boards.id"), default=None)
    original_board_id=Column(Integer, ForeignKey("boards.id"), default=None)
    over_18=Column(Boolean, default=False)
    original_board=relationship("Board", lazy="subquery", primaryjoin="Board.id==Submission.original_board_id")
    ban_reason=Column(String(128), default="")
    creation_ip=Column(String(64), default="")
    mod_approved=Column(Integer, default=None)
    accepted_utc=Column(Integer, default=0)
    is_image=Column(Boolean, default=False)
    has_thumb=Column(Boolean, default=False)
    post_public=Column(Boolean, default=True)
    score_hot=Column(Float, default=0)
    score_disputed=Column(Float, default=0)
    score_top=Column(Float, default=1)
    score_activity=Column(Float, default=0)

    approved_by=relationship("User", uselist=False, primaryjoin="Submission.is_approved==User.id")


    #These are virtual properties handled as postgres functions server-side
    #There is no difference to SQLAlchemy, but they cannot be written to

    ups = deferred(Column(Integer, server_default=FetchedValue()))
    downs=deferred(Column(Integer, server_default=FetchedValue()))
    age=deferred(Column(Integer, server_default=FetchedValue()))
    comment_count=Column(Integer, server_default=FetchedValue())
    flag_count=deferred(Column(Integer, server_default=FetchedValue()))
    report_count=deferred(Column(Integer, server_default=FetchedValue()))
    score=Column(Float, server_default=FetchedValue())
    is_public=Column(Boolean, server_default=FetchedValue())

    rank_hot=deferred(Column(Float, server_default=FetchedValue()))
    rank_fiery=deferred(Column(Float, server_default=FetchedValue()))
    rank_activity=deferred(Column(Float, server_default=FetchedValue()))    

    def __init__(self, *args, **kwargs):

        if "created_utc" not in kwargs:
            kwargs["created_utc"]=int(time.time())
            kwargs["created_str"]=time.strftime("%I:%M %p on %d %b %Y", time.gmtime(kwargs["created_utc"]))

        kwargs["creation_ip"]=request.remote_addr

        super().__init__(*args, **kwargs)
        
    def __repr__(self):
        return f"<Submission(id={self.id})>"


    @property
    #@cache.memoize(timeout=60)
    def domain_obj(self):
        if not self.domain_ref:
            return None
        
        return db.query(Domain).filter_by(id=self.domain_ref).first()


    @property
    @cache.memoize(timeout=60)
    def score_percent(self):
        try:
            return int((self.ups/(self.ups+self.downs))*100)
        except ZeroDivisionError:
            return 0


    @property
    def fullname(self):
        return f"t2_{self.base36id}"

    @property
    def permalink(self):
        return f"/post/{self.base36id}"

    @property
    def is_archived(self):

        now=int(time.time())

        cutoff=now-(60860*24*180)

        return self.created_utc < cutoff
                                      
    def rendered_page(self, comment=None, comment_info=None, v=None):

        #check for banned
        if self.is_deleted:
            template="submission_deleted.html"
        elif v and v.admin_level>=3:
            template="submission.html"
        elif self.is_banned:
            template="submission_banned.html"
        else:
            template="submission.html"

        private=not self.is_public and not self.board.can_view(v)
        
        if private and not self.author_id==v.id:
            abort(403)
        elif private:
            self.comments=[]
        else:
            #load and tree comments
            #calling this function with a comment object will do a comment permalink thing
            self.tree_comments(comment=comment)
        
        #return template
        return render_template(template,
                               v=v,
                               p=self,
                               sort_method=request.args.get("sort","Hot").capitalize(),
                               linked_comment=comment,
                               comment_info=comment_info)

    @property
    @lazy
    def author(self):
        return self.author_rel


    @property
    @lazy
    def domain(self):

        if not self.url:
            return "text post"
        domain= urlparse(self.url).netloc
        if domain.startswith("www."):
            domain=domain.split("www.")[1]
        return domain



    def tree_comments(self, comment=None):

        def tree_replies(thing, layer=1):

            thing.__dict__["replies"]=[]
            i=len(comments)-1
        
            while i>=0:
                if comments[i].parent_fullname==thing.fullname:
                    thing.__dict__["replies"].append(comments[i])
                    comments.pop(i)

                i-=1
                
            if layer <=8:
                for reply in thing.replies:
                    tree_replies(reply, layer=layer+1)
                
        ######
                
        if comment:
            self.replies=[comment]
            return



        #get sort type
        sort_type = request.args.get("sort","hot")


        #Treeing is done from the end because reasons, so these sort orders are reversed
        if sort_type=="hot":
            comments=self.comments.order_by(text("comments.score_hot ASC")).all()
        elif sort_type=="top":
            comments=self.comments.order_by(text("comments.score_top ASC")).all()
        elif sort_type=="new":
            comments=self.comments.order_by(text("comments.created_utc")).all()
        elif sort_type=="disputed":
            comments=self.comments.order_by(text("comments.score_disputed ASC")).all()
        elif sort_type=="random":
            c=self.comments.all()
            comments=random.sample(c, k=len(c))
        else:
            abort(422)



        tree_replies(self)

        


    @property
    def active_flags(self):
        if self.is_approved:
            return 0
        else:
            return self.flags.filter(Flag.created_utc>self.approved_utc).count()

    @property
    def active_reports(self):
        if self.mod_approved:
            return 0
        else:
            return self.reports.filter(Report.created_utc>self.accepted_utc).count()


    @property
    #@lazy
    def thumb_url(self):
    
        if self.domain=="i.ruqqus.com":
            return self.url
        elif self.has_thumb:
            return f"https://i.ruqqus.com/posts/{self.base36id}/thumb.png"
        elif self.is_image:
            return self.url
        else:
            return None

    def visibility_reason(self, v):

        if self.author_id==v.id:
            return "this is your content."
        elif self.board.has_mod(v):
            return f"you are a guildmaster of +{self.board.name}."
        elif self.board.has_contributor(v):
            return f"you are an approved contributor in +{self.board.name}."
        elif v.admin_level >= 4:
            return "you are a Ruqqus admin."

        
from werkzeug.security import generate_password_hash, check_password_hash
from flask import *
import time
from sqlalchemy import *
from sqlalchemy.orm import relationship, deferred
from os import environ
from secrets import token_hex
import random
import pyotp

from ruqqus.helpers.base36 import *
from ruqqus.helpers.security import *
from ruqqus.helpers.lazy import lazy
import ruqqus.helpers.aws as aws
from .votes import Vote
from .alts import Alt
from .titles import Title
from .submission import Submission
from .comment import Comment
from .boards import Board
from .board_relationships import *
from .mix_ins import *
from ruqqus.__main__ import Base, db, cache

class User(Base, Stndrd):

    __tablename__="users"
    id = Column(Integer, primary_key=True)
    username = Column(String, default=None)
    email = Column(String, default=None)
    passhash = Column(String, default=None)
    created_utc = Column(Integer, default=0)
    admin_level = Column(Integer, default=0)
    is_activated = Column(Boolean, default=False)
    over_18=Column(Boolean, default=False)
    creation_ip=Column(String, default=None)
    most_recent_ip=Column(String, default=None)
    submissions=relationship("Submission", lazy="dynamic", primaryjoin="Submission.author_id==User.id", backref="author_rel")
    comments=relationship("Comment", lazy="dynamic", primaryjoin="Comment.author_id==User.id")
    votes=relationship("Vote", lazy="dynamic", backref="users")
    commentvotes=relationship("CommentVote", lazy="dynamic", backref="users")
    bio=Column(String, default="")
    bio_html=Column(String, default="")
    badges=relationship("Badge", lazy="dynamic", backref="user")
    real_id=Column(String, default=None)
    notifications=relationship("Notification", lazy="dynamic", backref="user")
    referred_by=Column(Integer, default=None)
    is_banned=Column(Integer, default=0)
    ban_reason=Column(String, default="")
    login_nonce=Column(Integer, default=0)
    title_id=Column(Integer, ForeignKey("titles.id"), default=None)
    title=relationship("Title")
    has_profile=Column(Boolean, default=False)
    has_banner=Column(Boolean, default=False)
    reserved=Column(String(256), default=None)
    is_nsfw=Column(Boolean, default=False)
    tos_agreed_utc=Column(Integer, default=0)
    profile_nonce=Column(Integer, default=0)
    banner_nonce=Column(Integer, default=0)
    last_siege_utc=Column(Integer, default=0)
    mfa_secret=Column(String(16), default=None)

    moderates=relationship("ModRelationship", lazy="dynamic")
    banned_from=relationship("BanRelationship", lazy="dynamic", primaryjoin="BanRelationship.user_id==User.id")
    subscriptions=relationship("Subscription", lazy="dynamic")
    boards_created=relationship("Board", lazy="dynamic")
    contributes=relationship("ContributorRelationship", lazy="dynamic")

    following=relationship("Follow", lazy="dynamic", primaryjoin="Follow.user_id==User.id")
    followers=relationship("Follow", lazy="dynamic", primaryjoin="Follow.target_id==User.id")


    
    #properties defined as SQL server-side functions
    energy = deferred(Column(Integer, server_default=FetchedValue()))
    comment_energy = deferred(Column(Integer, server_default=FetchedValue()))
    referral_count=deferred(Column(Integer, server_default=FetchedValue()))
    follower_count=deferred(Column(Integer, server_default=FetchedValue()))



    def __init__(self, **kwargs):

        if "password" in kwargs:

            kwargs["passhash"]=self.hash_password(kwargs["password"])
            kwargs.pop("password")

        kwargs["created_utc"]=int(time.time())

        super().__init__(**kwargs)

        
    def validate_2fa(self, token):
        
        x=pyotp.TOTP(self.mfa_secret)
        return x.verify(token, valid_window=1)
    
    @property
    def boards_subscribed(self):

        boards= [x.board for x in self.subscriptions if x.is_active]
        return boards

    @property
    def age(self):
        return int(time.time())-self.created_utc
        
    @cache.memoize(timeout=300)
    def idlist(self, sort="hot", page=1, only=None, t=None):

        

        posts=db.query(Submission).filter_by(is_banned=False,
                                             is_deleted=False,
                                             stickied=False
                                             )


        if not self.over_18:
            posts=posts.filter_by(over_18=False)

        if only in [None, "none"]:
            board_ids=[x.board_id for x in self.subscriptions.filter_by(is_active=True).all()]
            user_ids=[x.target_id for x in self.following.all()]
            user_ids.append(self.id)
            posts=posts.filter(or_(Submission.board_id.in_(board_ids), Submission.author_id.in_(user_ids)))
        elif only=="guilds":
            board_ids=[x.board_id for x in self.subscriptions.filter_by(is_active=True).all()]
            posts=posts.filter(Submission.board_id.in_(board_ids))
        elif only=="users":
            user_ids=[x.target_id for x in self.following.all()]
            user_ids.append(self.id)
            posts=posts.filter(Submission.author_id.in_(user_ids))
        else:
            abort(422)

        if not self.admin_level >=4:
            #admins can see everything
            m=self.moderates.filter_by(invite_rescinded=False).subquery()
            c=self.contributes.subquery()
            posts=posts.join(m,
                             m.c.board_id==Submission.board_id,
                             isouter=True
                             ).join(c,
                                    c.c.board_id==Submission.board_id,
                                    isouter=True
                                    )
            posts=posts.filter(or_(Submission.author_id==self.id,
                                   Submission.is_public==True,
                                   m.c.board_id != None,
                                   c.c.board_id !=None))

        if t:
            now=int(time.time())
            if t=='day':
                cutoff=now-86400
            elif t=='week':
                cutoff=now-604800
            elif t=='month':
                cutoff=now-2592000
            elif t=='year':
                cutoff=now-31536000
            else:
                cutoff=0
                
            posts=posts.filter(Submission.created_utc >= cutoff)
                
            

        if sort=="hot":
            posts=posts.order_by(Submission.score_hot.desc())
        elif sort=="new":
            posts=posts.order_by(Submission.created_utc.desc())
        elif sort=="disputed":
            posts=posts.order_by(Submission.score_disputed.desc())
        elif sort=="top":
            posts=posts.order_by(Submission.score_top.desc())
        elif sort=="activity":
            posts=posts.order_by(Submission.score_activity.desc())
        else:
            abort(422)

        posts=[x.id for x in posts.offset(25*(page-1)).limit(26).all()]

        return posts

    def list_of_posts(self, ids):

        next_exists=(len(ids)==26)
        ids=ids[0:25]

        if ids:

            #assemble list of tuples
            i=1
            tups=[]
            for x in ids:
                tups.append((x,i))
                i+=1

            tups=str(tups).lstrip("[").rstrip("]")

            #hit db for entries
            posts=db.query(Submission
                           ).from_statement(
                               text(
                               f"""
                                select submissions.*, submissions.ups, submissions.downs
                                from submissions
                                join (values {tups}) as x(id, n) on submissions.id=x.id
                                order by x.n"""
                               )).all()
        else:
            posts=[]

        return posts, next_exists

    def rendered_subscription_page(self, sort="hot", page=1):

        only=request.args.get("only",None)        

        ids=self.idlist(sort=sort,
                        page=page,
                        only=only,
                        t=request.args.get('t', None)
                        )

        posts, next_exists = self.list_of_posts(ids)
        
        #If page 1, check for sticky
        if page==1:
            sticky =[]
            sticky=db.query(Submission).filter_by(stickied=True).first()
            if sticky:
                posts=[sticky]+posts

        return render_template("subscriptions.html",
                               v=self,
                               listing=posts,
                               next_exists=next_exists,
                               sort_method=sort,
                               page=page,
                               only=only)        

    @property
    def mods_anything(self):

        return bool(self.moderates.filter_by(accepted=True).first())


    @property
    def boards_modded(self):

        return [x.board for x in self.moderates.filter_by(accepted=True).all()]

    @property
    @cache.memoize(timeout=3600) #1hr cache time for user rep
    def karma(self):
        return self.energy

    @property
    @cache.memoize(timeout=3600)
    def comment_karma(self):
        return self.comment_energy


    @property
    def base36id(self):
        return base36encode(self.id)

    @property
    def fullname(self):
        return f"t1_{self.base36id}"

    @property
    @cache.memoize(timeout=60)
    def has_report_queue(self):
        board_ids=[x.board_id for x in self.moderates.filter_by(accepted=True).all()]
        return bool(db.query(Submission).filter(Submission.board_id.in_(board_ids), Submission.mod_approved==0, Submission.report_count>=1).first())

    @property
    def banned_by(self):

        if not self.is_banned:
            return None

        return db.query(User).filter_by(id=self.is_banned).first()

    def has_badge(self, badgedef_id):
        return self.badges.filter_by(badge_id=badgedef_id).first()
    
    def vote_status_on_post(self, post):

        vote = self.votes.filter_by(submission_id=post.id).first()
        if not vote:
            return 0
        
        return vote.vote_type


    def vote_status_on_comment(self, comment):

        vote = self.commentvotes.filter_by(comment_id=comment.id).first()
        if not vote:
            return 0
        
        return vote.vote_type
    

    def hash_password(self, password):
        return generate_password_hash(password, method='pbkdf2:sha512', salt_length=8)

    def verifyPass(self, password):
        return check_password_hash(self.passhash, password)
        
    def rendered_userpage(self, v=None):

        if self.reserved:
            return render_template("userpage_reserved.html", u=self, v=v)

        if self.is_banned and (not v or v.admin_level < 3):
            return render_template("userpage_banned.html", u=self, v=v)

        page=int(request.args.get("page","1"))
        page=max(page, 1)

        submissions=self.submissions

        if not (v and v.over_18):
            submissions=submissions.filter_by(over_18=False)

        if not (v and (v.admin_level >=3)):
            submissions=submissions.filter_by(is_deleted=False)

        if not (v and (v.admin_level >=3 or v.id==self.id)):
            submissions=submissions.filter_by(is_banned=False)




        if v and v.admin_level >=4:
            pass
        elif v:
            m=v.moderates.filter_by(invite_rescinded=False).subquery()
            c=v.contributes.subquery()
            
            submissions=submissions.join(m,
                                         m.c.board_id==Submission.board_id,
                                         isouter=True
                                    ).join(c,
                                           c.c.board_id==Submission.board_id,
                                           isouter=True
                                    )
            submissions=submissions.filter(or_(Submission.author_id==v.id,
                                   Submission.is_public==True,
                               m.c.board_id != None,
                               c.c.board_id !=None))
        else:
            submissions=submissions.filter_by(is_public=True)

            

        listing = [x for x in submissions.order_by(Submission.created_utc.desc()).offset(25*(page-1)).limit(26)]
        
        #we got 26 items just to see if a next page exists
        next_exists=(len(listing)==26)
        listing=listing[0:25]

        is_following=(v and self.has_follower(v))

        return render_template("userpage.html",
                               u=self,
                               v=v,
                               listing=listing,
                               page=page,
                               next_exists=next_exists,
                               is_following=is_following)

    def rendered_comments_page(self, v=None):
        if self.reserved:
            return render_template("userpage_reserved.html", u=self, v=v)

        if self.is_banned and (not v or v.admin_level < 3):
            return render_template("userpage_banned.html", u=self, v=v)
        
        page=int(request.args.get("page","1"))

        comments=self.comments.filter(text("parent_submission is not null"))

        if not (v and v.over_18):
            comments=comments.filter_by(over_18=False)
            
        if not (v and (v.admin_level >=3)):
            comments=comments.filter_by(is_deleted=False)
            
        if not (v and (v.admin_level >=3 or v.id==self.id)):
            comments=comments.filter_by(is_banned=False)

        if v and v.admin_level >= 4:
            pass
        elif v:
            m=v.moderates.filter_by(invite_rescinded=False).subquery()
            c=v.contributes.subquery()
            
            comments=comments.join(m,
                                   m.c.board_id==Comment.board_id,
                                   isouter=True
                         ).join(c,
                                c.c.board_id==Comment.board_id,
                                isouter=True
                                )
            comments=comments.filter(or_(Comment.author_id==v.id,
                                   Comment.is_public==True,
                               m.c.board_id != None,
                               c.c.board_id !=None))
        else:
            comments=comments.filter_by(is_public=True)

        comments=comments.order_by(Comment.created_utc.desc())
        comments=comments.offset(25*(page-1)).limit(26)
        

        listing=[c for c in comments]
        #we got 26 items just to see if a next page exists
        next_exists=(len(listing)==26)
        listing=listing[0:25]

        is_following=(v and self.has_follower(v))
        
        return render_template("userpage_comments.html",
                               u=self,
                               v=v,
                               listing=listing,
                               page=page,
                               next_exists=next_exists,
                               is_following=is_following)

    @property
    def formkey(self):

        if "session_id" not in session:
            session["session_id"]=token_hex(16)

        msg=f"{session['session_id']}+{self.id}+{self.login_nonce}"

        return generate_hash(msg)

    def validate_formkey(self, formkey):

        return validate_hash(f"{session['session_id']}+{self.id}+{self.login_nonce}", formkey)
    
    @property
    def url(self):
        return f"/@{self.username}"

    @property
    def permalink(self):
        return self.url

    @property
    @lazy
    def created_date(self):

        return time.strftime("%d %B %Y", time.gmtime(self.created_utc))

    def __repr__(self):
        return f"<User(username={self.username})>"

    def notifications_page(self, page=1, include_read=False):

        page=int(page)

        notifications=self.notifications.filter_by(is_banned=False, is_deleted=False)

        if not include_read:
            notifications=notifications.filter_by(read=False)

        notifications = notifications.order_by(text("id desc")).offset(25*(page-1)).limit(26)

        comments=[n.comment for n in notifications]
        next_exists=(len(comments)==26)
        comments=comments[0:25]

        for n in [x for x in notifications][0:25]:
            if not n.read:
                n.read=True
                db.add(n)
                db.commit()

        return render_template("notifications.html",
                               v=self,
                               notifications=comments,
                               next_exists=next_exists,
                               page=page)
    
    @property
    def notifications_count(self):

        return self.notifications.filter_by(read=False, is_banned=False, is_deleted=False).count()

    @property
    def post_count(self):

        return self.submissions.filter_by(is_banned=False).count()

    @property
    def comment_count(self):

        return self.comments.filter(text("parent_submission is not null")).filter_by(is_banned=False, is_deleted=False).count()

    @property
    #@cache.memoize(timeout=60)
    def badge_pairs(self):

        output=[]

        badges=[x for x in self.badges.all()]

        while badges:
            
            to_append=[badges.pop(0)]
            
            if badges:
                to_append.append(badges.pop(0))
                
            output.append(to_append)

        return output

    @property
    def alts(self):

        alts1=db.query(User).join(Alt, Alt.user2==User.id).filter(Alt.user1==self.id).all()
        alts2=db.query(User).join(Alt, Alt.user1==User.id).filter(Alt.user2==self.id).all()

        return list(set([x for x in alts1]+[y for y in alts2]))
        

    def has_follower(self, user):

        return self.followers.filter_by(user_id=user.id).first()

    def set_profile(self, file):

        self.del_profile()
        self.profile_nonce+=1

        aws.upload_file(name=f"users/{self.username}/profile-{self.profile_nonce}.png",
                        file=file)
        self.has_profile=True
        db.add(self)
        db.commit()
        
    def set_banner(self, file):

        self.del_banner()
        self.banner_nonce+=1

        aws.upload_file(name=f"users/{self.username}/banner-{self.banner_nonce}.png",
                        file=file)

        self.has_banner=True
        db.add(self)
        db.commit()

    def del_profile(self):

        aws.delete_file(name=f"users/{self.username}/profile-{self.profile_nonce}.png")
        self.has_profile=False
        db.add(self)
        db.commit()

    def del_banner(self):

        aws.delete_file(name=f"users/{self.username}/banner-{self.banner_nonce}.png")
        self.has_banner=False
        db.add(self)
        db.commit()

    @property
    def banner_url(self):

        if self.has_banner:
            return f"https://i.ruqqus.com/users/{self.username}/banner-{self.banner_nonce}.png"
        else:
            return "/assets/images/profiles/default_bg.png"

    @property
    def profile_url(self):

        if self.has_profile:
            return f"https://i.ruqqus.com/users/{self.username}/profile-{self.profile_nonce}.png"
        else:
            return "/assets/images/profiles/default-profile-pic.png"

    @property
    def available_titles(self):

        locs={"v":self,
              "Board":Board,
              "Submission":Submission
              }

        titles=[i for i in db.query(Title).order_by(text("id asc")).all() if eval(i.qualification_expr,{}, locs)]
        return titles

    @property
    def can_make_guild(self):

        if self.karma + self.comment_karma < 50:
            return False

        return True

    @property
    def can_siege(self):

        if self.is_banned:
            return False

        now=int(time.time())

        return now-self.last_siege_utc > 60*60*24*30
from flask import *
import time
from sqlalchemy import *
from sqlalchemy.orm import relationship, deferred
from random import randint
import math
from .mix_ins import *
from ruqqus.helpers.base36 import *
from ruqqus.helpers.lazy import lazy
from ruqqus.__main__ import Base, db, cache
from .submission import Submission
from .votes import CommentVote
from .flags import CommentFlag
from .boards import Board

class Comment(Base, Age_times, Scores, Stndrd):

    __tablename__="comments"

    id = Column(Integer, primary_key=True)
    author_id = Column(Integer, ForeignKey("users.id"))
    body = Column(String(2000), default=None)
    parent_submission = Column(Integer, ForeignKey("submissions.id"))
    parent_fullname = Column(Integer) #this column is foreignkeyed to comment(id) but we can't do that yet as "comment" class isn't yet defined
    created_utc = Column(Integer, default=0)
    edited_utc = Column(Integer, default=0)
    is_banned = Column(Boolean, default=False)
    body_html = Column(String)
    distinguish_level=Column(Integer, default=0)
    is_deleted = Column(Boolean, default=False)
    is_approved = Column(Integer, default=0)
    approved_utc=Column(Integer, default=0)
    ban_reason=Column(String(256), default='')
    creation_ip=Column(String(64), default='')
    score_disputed=Column(Float, default=0)
    score_hot=Column(Float, default=0)
    score_top=Column(Integer, default=1)

    post=relationship("Submission", lazy="subquery")
    flags=relationship("CommentFlag", lazy="dynamic", backref="comment")
    author=relationship("User", lazy="subquery", primaryjoin="User.id==Comment.author_id")

    #These are virtual properties handled as postgres functions server-side
    #There is no difference to SQLAlchemy, but they cannot be written to
    ups = deferred(Column(Integer, server_default=FetchedValue()))
    downs=deferred(Column(Integer, server_default=FetchedValue()))
    age=Column(Integer, server_default=FetchedValue())
    is_public=Column(Boolean, server_default=FetchedValue())

    score=deferred(Column(Integer, server_default=FetchedValue()))
    

    rank_fiery=deferred(Column(Float, server_default=FetchedValue()))
    rank_hot=deferred(Column(Float, server_default=FetchedValue()))

    flag_count=deferred(Column(Integer, server_default=FetchedValue()))
    over_18=Column(Boolean, server_default=FetchedValue())

    board_id=Column(Integer, server_default=FetchedValue())
    
    

    def __init__(self, *args, **kwargs):
                   

        if "created_utc" not in kwargs:
            kwargs["created_utc"]=int(time.time())

        kwargs["creation_ip"]=request.remote_addr
            
        super().__init__(*args, **kwargs)
                
    def __repr__(self):

        return f"<Comment(id={self.id})"

    @property
    def fullname(self):
        return f"t3_{self.base36id}"

    @property
    def is_top_level(self):
        return self.parent_fullname.startswith("t2_")



    @property
    @lazy
    def board(self):

        if self.post:
            return self.post.board
        else:
            return None
    
    @property
    @lazy
    def parent(self):

        if not self.parent_submission:
            return None

        if self.is_top_level:
            return db.query(Submission).filter_by(id=self.parent_submission).first()
        else:
            return db.query(Comment).filter_by(id=base36decode(self.parent_fullname.split(sep="_")[1])).first()

    @property
    def children(self):

        return db.query(Comment).filter_by(parent_comment=self.id).all()

    @property
    def replies(self):

        if "replies" in self.__dict__:
            return self.__dict__["replies"]
        else:
            return db.query(Comment).filter_by(parent_fullname=self.fullname).all()

    @property
    def permalink(self):

        return f"/post/{self.post.base36id}/comment/{self.base36id}"

    @property
    @cache.memoize(timeout=60)
    def any_descendants_live(self):

        if self.replies==[]:
            return False

        if any([not x.is_banned and not x.is_deleted for x in self.replies]):
            return True

        else:
            return any([x.any_descendants_live for x in self.replies])
        

    def rendered_comment(self, v=None, render_replies=True, standalone=False, level=1):

        if self.is_banned or self.is_deleted:
            if v and v.admin_level>1:
                return render_template("single_comment.html", v=v, c=self, replies=self.replies, render_replies=render_replies, standalone=standalone, level=level)
                
            elif self.any_descendants_live:
                return render_template("single_comment_removed.html", c=self, replies=self.replies, render_replies=render_replies, standalone=standalone, level=level)
            else:
                return ""

        return render_template("single_comment.html", v=v, c=self, replies=self.replies, render_replies=render_replies, standalone=standalone, level=level)

    @property
    def active_flags(self):
        if self.is_approved:
            return 0
        else:
            return self.flag_count

    def visibility_reason(self, v):
        if self.author_id==v.id:
            return "this is your content."
        elif self.board.has_mod(v):
            return f"you are a guildmaster of +{self.board.name}."
        elif self.board.has_contributor(v):
            return f"you are an approved contributor in +{self.board.name}."
        elif self.parent.author_id==v.id:
            return "this is a reply to your content."
        elif v.admin_level >= 4:
            return "you are a Ruqqus admin."

    @property
    @cache.memoize(timeout=60)
    def score_fuzzed(self):
        
        k=0.01
        real = self.score_top
        a = math.floor(real * (1 - k))
        b = math.ceil(real * (1 + k))
        return random.randint(a, b)
        
class Notification(Base):

    __tablename__="notifications"

    id=Column(Integer, primary_key=True)
    user_id=Column(Integer, ForeignKey("users.id"))
    comment_id=Column(Integer, ForeignKey("comments.id"))
    read=Column(Boolean, default=False)

    #Server side computed values (copied from corresponding comment)
    created_utc=Column(Integer, server_default=FetchedValue())
    is_banned=Column(Boolean, server_default=FetchedValue())
    is_deleted=Column(Boolean, server_default=FetchedValue())

    def __repr__(self):

        return f"<Notification(id={self.id})"

    @property
    def comment(self):

        return db.query(Comment).filter_by(id=self.comment_id).first()

    @property
    def board(self):

        return db.query(Board).filter_by(id=self.board_id).first()
View our code
        import time
        from ruqqus.helpers.wrappers
        import *
        from flask
        import *

        from ruqqus.__main__
        import app, db, cache
        from ruqqus.classes
        import *

        @cache.memoize(timeout = 30)
        def frontlist(sort = "hot", page = 1):

        cutoff = int(time.time()) - (60 * 60 * 24 * 30)

        posts = db.query(Submission).filter(Submission.created_utc >= cutoff,
        Submission.is_banned == False,
        Submission.stickied == False)

        if sort == "hot":
        posts = posts.order_by(text("submissions.rank_hot desc"))
        elif sort == "new":
        posts = posts.order_by(Submission.created_utc.desc())
        elif sort == "fiery":
        posts = posts.order_by(text("submissions.rank_fiery desc"))
        elif sort == "top":
        posts = posts.order_by(text("submissions.score desc"))

        posts = [x.id
        for x in posts.offset(25 * (page - 1)).limit(25).all()
        ]

        return posts

        @app.route("/", methods = ["GET"])
        @auth_desired
        def home(v):

        page = int(request.args.get("page", 1))

        sort_method = request.args.get("sort", "hot")

        # get list of ids
        ids = frontlist(sort = sort_method, page = page)

        # assemble list of tuples
        i = 1
        tups = []
        for x in ids:
        tups.append((x, i))
        i += 1

        # tuple string
        tups = str(tups).lstrip("[").rstrip("]")

        # hit db
        for entries
        posts = db.query(Submission).from_statement(
        text(f ""
        "
        select submissions.*, submissions.ups, submissions.downs from submissions join(values {
        tups
    }) as x(id, n) on submissions.id = x.id order by x.n ""
    "
    )).all()

    # If page 1, check
    for sticky
    if page == 1:
    sticky = []
    sticky = db.query(Submission).filter_by(stickied = True).first()
    if sticky:
    posts = [sticky] + posts

    return render_template("home.html", v = v, listing = posts, sort_method = sort_method)

get started

Join the future of online discourse and make a Ruqqus

Create a Ruqqus account in seconds and join your favorite communities. Usernames aplenty. No email required.

Sign up
character/moshing Created with Sketch.