Support online freedom of speech and the future of digital discourse

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

Donate to TalkSpar

Just want to use TalkSpar? Visit talkspar.com

Transparency

Support a fair and open platform

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 TalkSpar.

View our code
from flask import g, request
from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint
from sqlalchemy.orm import relationship

from apps.__main__ import Base


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"), index=True)
    created_utc = Column(Integer, default=0)
    creation_ip = Column(String, default=None)
    app_id = Column(Integer, ForeignKey("oauth_apps.id"), default=None)

    unique_votes = UniqueConstraint("user_id", "submission_id")

    user = relationship("User", lazy="subquery", backref="votes")
    post = relationship("Submission", lazy="subquery")

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

        if "created_utc" not in kwargs:
            kwargs["created_utc"] = g.timestamp

        kwargs["creation_ip"]=request.remote_addr

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

    def __repr__(self):
        return f"<Vote(id={self.id})>"

    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 = g.timestamp

        g.db.add(self)

    @property
    def json_core(self):
        data={
            "user_id": self.user_id,
            "submission_id":self.submission_id,
            "created_utc": self.created_utc,
            "vote_type":self.vote_type
            }
        return data

    @property
    def json(self):
        data=self.json_core
        data["user"]=self.user.json_core
        data["post"]=self.post.json_core
    
        return data


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"), index=True)
    created_utc = Column(Integer, default=0)
    creation_ip = Column(String, default=None)
    app_id = Column(Integer, ForeignKey("oauth_apps.id"), default=None)

    unique_votes = UniqueConstraint("user_id", "comment_id")

    user = relationship("User", lazy="subquery", backref="commentvotes")
    comment = relationship("Comment", lazy="subquery")

    def __init__(self, *args, **kwargs):
        if "created_utc" not in kwargs:
            kwargs["created_utc"] = g.timestamp

        kwargs["creation_ip"]=request.remote_addr

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

    def __repr__(self):
        return f"<CommentVote(id={self.id})>"

    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 = g.timestamp

        g.db.add(self)

    @property
    def json_core(self):
        data={
            "user_id": self.user_id,
            "comment_id":self.comment_id,
            "created_utc": self.created_utc,
            "vote_type":self.vote_type
            }
        return data

    @property
    def json(self):
        data=self.json_core
        data["user"]=self.user.json_core
        data["comment"]=self.comment.json_core
    
        return data
from flask import render_template, request, abort, g
import time
from sqlalchemy import (
    Column,
    Integer,
    BigInteger,
    String,
    Boolean,
    ForeignKey,
    FetchedValue,
    Index,
    Float,
)
from sqlalchemy.orm import relationship, deferred
import math
from urllib.parse import urlparse
import random
from os import environ
import requests
import re
from .mix_ins import *
from apps.helpers.base36 import base36encode
from apps.helpers.lazy import lazy
import apps.helpers.aws as aws
from apps.__main__ import Base, cache, app, debug

# from .votes import Vote, CommentVote
from .domains import Domain
from .flags import Flag
from .badwords import BadWord
from .comment import Comment


class SubmissionAux(Base):
    __tablename__ = "submissions_aux"

    # we don't care about this ID
    key_id = Column(BigInteger, primary_key=True)
    id = Column(BigInteger, ForeignKey("submissions.id"), index=True, unique=True)
    title = Column(String(500), default=None)
    url = Column(String(2048), default=None)
    body = Column(String(10000), default="")
    body_html = Column(String(20000), default="")
    ban_reason = Column(String(128), default="")
    embed_url = Column(String(3000), default="")
    meta_title = Column(String(512), default="")
    meta_description = Column(String(1024), default="")

    __table_args__ = (
        Index(
            "submissions_aux_url_trgm_idx",
            "url",
            postgresql_using="gin",
            postgresql_ops={"url": "gin_trgm_ops"},
        ),
        Index(
            "submissions_aux_title_trgm_idx",
            "title",
            postgresql_using="gin",
            postgresql_ops={"title": "gin_trgm_ops"},
        ),
        Index(
            "submissions_aux_body_trgm_idx",
            "body",
            postgresql_using="gin",
            postgresql_ops={"body": "gin_trgm_ops"},
        ),
    )


class Submission(Base, standard_mixin, age_mixin, score_mixin, fuzzing_mixin):
    __tablename__ = "submissions"

    id = Column(BigInteger, primary_key=True)
    submission_aux = relationship(
        "SubmissionAux",
        lazy="joined",
        uselist=False,
        innerjoin=True,
        primaryjoin="Submission.id==SubmissionAux.id",
    )
    author_id = Column(BigInteger, ForeignKey("users.id"), index=True)
    repost_id = Column(BigInteger, ForeignKey("submissions.id"), default=None)
    edited_utc = Column(BigInteger, default=0)
    created_utc = Column(BigInteger, default=0)
    is_banned = Column(Boolean, default=False)
    deleted_utc = Column(Integer, default=0)
    purged_utc = Column(Integer, default=0)
    distinguish_level = Column(Integer, default=0)
    gm_distinguish = Column(Integer, ForeignKey("boards.id"), default=None)
    distinguished_board = relationship(
        "Board", lazy="joined", primaryjoin="Board.id==Submission.gm_distinguish"
    )
    created_str = Column(String(255), default=None)
    stickied = Column(Boolean, default=False)
    # _comments = relationship(
    #     "Comment",
    #     lazy="dynamic",
    #     primaryjoin="Comment.parent_submission==Submission.id",
    #     backref="submissions")
    domain_ref = Column(Integer, ForeignKey("domains.id"), default=None)
    domain_obj = relationship("Domain")
    flags = relationship("Flag", backref="submission")
    is_approved = Column(Integer, ForeignKey("users.id"), default=None)
    approved_utc = Column(Integer, default=0)
    board_id = Column(Integer, ForeignKey("boards.id"), default=None, index=True)
    original_board_id = Column(Integer, ForeignKey("boards.id"), default=None)
    over_18 = Column(Boolean, default=False)
    original_board = relationship(
        "Board", primaryjoin="Board.id==Submission.original_board_id"
    )
    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)
    is_offensive = Column(Boolean, default=False)
    is_nsfl = Column(Boolean, default=False)
    board = relationship(
        "Board",
        lazy="joined",
        innerjoin=True,
        primaryjoin="Submission.board_id==Board.id",
        viewonly=True,
    )
    author = relationship(
        "User",
        lazy="joined",
        innerjoin=True,
        primaryjoin="Submission.author_id==User.id",
        viewonly=True,
    )
    is_pinned = Column(Boolean, default=False)
    score_best = Column(Float, default=0)
    reports = relationship("Report", backref="submission")
    is_bot = Column(Boolean, default=False)

    upvotes = Column(Integer, default=1, nullable=False)
    downvotes = Column(Integer, default=0, nullable=False)
    creation_region = Column(String(2), default=None)

    app_id = Column(Integer, ForeignKey("oauth_apps.id"), default=None)
    oauth_app = relationship("OauthApp")

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

    # not sure if we need this
    reposts = relationship("Submission", lazy="joined", remote_side=[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 = deferred(Column(Float, server_default=FetchedValue()))
    # is_public=deferred(Column(Boolean, server_default=FetchedValue()))

    awards = relationship("AwardRelationship", lazy="joined")

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

    scores_last_updated_utc = Column(Integer, default=0)

    # ordered indexes on scores
    __table_args__ = (
        Index(
            "submissions_score_hot_desc_idx",
            "score_hot",
            postgresql_using="btree",
            postgresql_ops={"score_hot": "DESC"},
        ),
        Index(
            "submissions_score_disputed_desc_idx",
            "score_disputed",
            postgresql_using="btree",
            postgresql_ops={"score_disputed": "DESC"},
        ),
        Index(
            "submissions_score_top_desc_idx",
            "score_top",
            postgresql_using="btree",
            postgresql_ops={"score_top": "DESC"},
        ),
        Index(
            "submissions_score_activity_desc_idx",
            "score_activity",
            postgresql_using="btree",
            postgresql_ops={"score_activity": "DESC"},
        ),
        Index(
            "submissions_created_utc_desc_idx",
            "created_utc",
            postgresql_using="btree",
            postgresql_ops={"created_utc": "DESC"},
        ),
    )

    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
    @lazy
    def board_base36id(self):
        return base36encode(self.board_id)

    @property
    @lazy
    def is_deleted(self):
        return bool(self.deleted_utc)

    @property
    def is_repost(self):
        return bool(self.repost_id)

    @property
    def is_archived(self):
        return int(time.time()) - self.created_utc > 60 * 60 * 24 * 180

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

    @property
    @lazy
    def permalink(self):
        output = self.title.lower()

        output = re.sub("&\w{2,3};", "", output)

        output = [re.sub("\W", "", word) for word in output.split()]
        output = [x for x in output if x][0:6]

        output = "-".join(output)

        if not output:
            output = "-"

        return f"/+{self.board.name}/post/{self.base36id}/{output}"

    @property
    @lazy
    def votes_permalink(self):
        return self.permalink.replace("/post/", "/votes/", 1)

    @property
    def is_archived(self):
        now = int(time.time())

        cutoff = now - (60 * 60 * 24 * 180)

        return self.created_utc < cutoff

    def rendered_page(self, comment=None, comment_info=None):
        # check for banned
        if self.deleted_utc > 0:
            template = "submission_deleted.html"
        elif g.user and g.user.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.is_pinned
            and not self.board.can_view(g.user)
        )

        if private and (not g.user or not self.author_id == g.user.id):
            abort(403)
        elif private:
            self.__dict__["replies"] = []
        else:
            # load and tree comments
            # calling this function with a comment object will do a comment
            # permalink thing
            if (
                "replies" not in self.__dict__
                and "_preloaded_comments" in self.__dict__
            ):
                self.tree_comments(comment=comment)

        # return template
        is_allowed_to_comment = self.board.can_comment(g.user) and not self.is_archived

        #    if request.args.get("sort", "Hot") != "new":
        #        self.replies = [x for x in self.replies if x.is_pinned] + [x for x in self.replies if not x.is_pinned]

        return render_template(
            template,
            p=self,
            sort_method=request.args.get("sort", "Hot").capitalize(),
            linked_comment=comment,
            comment_info=comment_info,
            is_allowed_to_comment=is_allowed_to_comment,
            render_replies=True,
            b=self.board,
        )

    @property
    @lazy
    def domain(self):
        if not self.url:
            return "text post"
        domain = urlparse(self.url).netloc
        if domain.startswith("www."):
            domain = domain[4:]
        return domain

    def tree_comments(self, comment=None):
        comments = self.__dict__.get("_preloaded_comments", [])
        if not comments:
            return

        pinned_comment = []

        index = {}
        for c in comments:
            if c.is_pinned and c.parent_fullname == self.fullname:
                pinned_comment = [c]
                continue

            if c.parent_fullname in index:
                index[c.parent_fullname].append(c)
            else:
                index[c.parent_fullname] = [c]

        for c in comments:
            c.__dict__["replies"] = index.get(c.fullname, [])

        if comment:
            self.__dict__["replies"] = [comment]
        else:
            self.__dict__["replies"] = pinned_comment + index.get(self.fullname, [])

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

    @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.has_thumb:
            return (
                f"https://{app.config['S3_BUCKET_URL']}/posts/{self.base36id}/thumb.png"
            )
        elif self.is_image:
            return self.url
        elif self.embed_url:
            return "/assets/images/icons/default_thumb_yt.png"
        else:
            return None

    @property
    def visibility_reason(self):
        if g.user and self.author_id == g.user.id:
            return "this is your content."
        elif self.is_pinned:
            return "a guildmaster has pinned it."
        elif self.board.has_mod(g.user):
            return f"you are a guildmaster of +{self.board.name}."
        elif self.board.has_contributor(g.user):
            return f"you are an approved contributor in +{self.board.name}."
        elif g.user.admin_level >= 4:
            return f"you are a {app.config['SITE_NAME']} admin."

    def determine_offensive(self):
        for x in g.db.query(BadWord).all():
            if (self.body and x.check(self.body)) or x.check(self.title):
                self.is_offensive = True
                break
        else:
            self.is_offensive = False

    @property
    @lazy
    def is_crosspost(self):
        return bool(
            (self.domain == app.config["SERVER_NAME"])
            and re.match(
                "^https?://[a-zA-Z0-9_.-]+/\+\w+/post/(\w+)(/[a-zA-Z0-9_-]+/?)?$",
                self.url,
            )
        )

    @property
    def json_raw(self):
        data = {
            "author_name": self.author.username if not self.author.is_deleted else None,
            "permalink": self.permalink,
            "is_banned": bool(self.is_banned),
            "is_deleted": self.is_deleted,
            "created_utc": self.created_utc,
            "id": self.base36id,
            "fullname": self.fullname,
            "title": self.title,
            "is_nsfw": self.over_18,
            "is_nsfl": self.is_nsfl,
            "is_bot": self.is_bot,
            "thumb_url": self.thumb_url,
            "domain": self.domain,
            "is_archived": self.is_archived,
            "url": self.url,
            "body": self.body,
            "body_html": self.body_html,
            "created_utc": self.created_utc,
            "edited_utc": self.edited_utc or 0,
            "guild_name": self.board.name,
            "original_guild_name": self.original_board.name
            if not self.board_id == self.original_board_id
            else None,
            "original_guild_id": self.original_board.id
            if not self.board_id == self.original_board_id
            else None,
            "guild_id": base36encode(self.board_id),
            "comment_count": self.comment_count,
            "score": self.score_fuzzed,
            "upvotes": self.upvotes_fuzzed,
            "downvotes": self.downvotes_fuzzed,
            "award_count": self.award_count,
            "is_offensive": self.is_offensive,
            "meta_title": self.meta_title,
            "meta_description": self.meta_description,
            "is_pinned": self.is_pinned,
            "is_distinguished": bool(self.distinguish_level),
            "is_heralded": bool(self.gm_distinguish),
            "is_crosspost": self.is_crosspost,
            "herald_guild": self.distinguished_board.name
            if self.gm_distinguish
            else None,
        }
        if self.ban_reason:
            data["ban_reason"] = self.ban_reason

        if self.board_id != self.original_board_id and self.original_board:
            data["original_guild_name"] = self.original_board.name
            data["original_guild_id"] = base36encode(self.original_board_id)
        return data

    @property
    def json_core(self):
        if self.is_banned:
            return {
                "is_banned": True,
                "is_deleted": self.is_deleted,
                "ban_reason": self.ban_reason,
                "id": self.base36id,
                "title": self.title,
                "permalink": self.permalink,
                "guild_name": self.board.name,
                "is_pinned": self.is_pinned,
            }
        elif self.is_deleted:
            return {
                "is_banned": bool(self.is_banned),
                "is_deleted": True,
                "id": self.base36id,
                "title": self.title,
                "permalink": self.permalink,
                "guild_name": self.board.name,
            }

        return self.json_raw

    @property
    def json(self):
        data = self.json_core

        if self.deleted_utc > 0 or self.is_banned:
            return data

        data["author"] = self.author.json_core
        data["guild"] = self.board.json_core
        data["original_guild"] = (
            self.original_board.json_core
            if not self.board_id == self.original_board_id
            else None
        )
        data["comment_count"]: self.comment_count

        if "replies" in self.__dict__:
            data["replies"] = [x.json_core for x in self.replies]

        if "_voted" in self.__dict__:
            data["voted"] = self._voted

        return data

    @property
    def voted(self):
        return self.__dict__.get("_voted", 0)

    @property
    def title(self):
        return self.submission_aux.title

    @title.setter
    def title(self, x):
        self.submission_aux.title = x
        g.db.add(self.submission_aux)

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

    @url.setter
    def url(self, x):
        self.submission_aux.url = x
        g.db.add(self.submission_aux)

    @property
    def body(self):
        return self.submission_aux.body

    @body.setter
    def body(self, x):
        self.submission_aux.body = x
        g.db.add(self.submission_aux)

    @property
    def body_html(self):
        return self.submission_aux.body_html

    @body_html.setter
    def body_html(self, x):
        self.submission_aux.body_html = x
        g.db.add(self.submission_aux)

    @property
    def ban_reason(self):
        return self.submission_aux.ban_reason

    @ban_reason.setter
    def ban_reason(self, x):
        self.submission_aux.ban_reason = x
        g.db.add(self.submission_aux)

    @property
    def embed_url(self):
        return self.submission_aux.embed_url

    @embed_url.setter
    def embed_url(self, x):
        self.submission_aux.embed_url = x
        g.db.add(self.submission_aux)

    @property
    def meta_title(self):
        return self.submission_aux.meta_title

    @meta_title.setter
    def meta_title(self, x):
        self.submission_aux.meta_title = x
        g.db.add(self.submission_aux)

    @property
    def meta_description(self):
        return self.submission_aux.meta_description

    @meta_description.setter
    def meta_description(self, x):
        self.submission_aux.meta_description = x
        g.db.add(self.submission_aux)

    def is_guildmaster(self, perm=None):
        mod = self.__dict__.get("_is_guildmaster", False)

        if not mod:
            return False
        elif not perm:
            return True
        else:
            return mod.perm_full or mod.__dict__[f"perm_{perm}"]

        return output

    @property
    def is_blocking_guild(self):
        return self.__dict__.get("_is_blocking_guild", False)

    @property
    def is_blocked(self):
        return self.__dict__.get("_is_blocked", False)

    @property
    def is_blocking(self):
        return self.__dict__.get("_is_blocking", False)

    @property
    def is_subscribed(self):
        return self.__dict__.get("_is_subscribed", False)

    @property
    def is_public(self):
        return self.post_public or not self.board.is_private

    @property
    def flag_count(self):
        return len(self.flags)

    @property
    def report_count(self):
        return len(self.reports)

    @property
    def award_count(self):
        return len(self.awards)

    @property
    def embed_template(self):
        return f"site_embeds/{self.domain_obj.embed_template}.html"

    @property
    def self_download_json(self):
        # This property should never be served to anyone but author and admin
        if not self.is_banned and self.deleted_utc == 0:
            return self.json_core

        data = {
            "title": self.title,
            "author": self.author.username,
            "url": self.url,
            "body": self.body,
            "body_html": self.body_html,
            "is_banned": bool(self.is_banned),
            "deleted_utc": self.deleted_utc,
            "created_utc": self.created_utc,
            "id": self.base36id,
            "fullname": self.fullname,
            "guild_name": self.board.name,
            "comment_count": self.comment_count,
            "permalink": self.permalink,
        }

        if self.original_board_id and (self.original_board_id != self.board_id):
            data["original_guild_name"] = self.original_board.name

        return data

    @property
    def json_admin(self):
        data = self.json_raw

        data["creation_ip"] = self.creation_ip
        data["creation_region"] = self.creation_region

        return data

    @property
    def is_exiled_for(self):
        return self.__dict__.get("_is_exiled_for", None)

    @property
    def is_image(self):
        return self.has_thumb and self.domain_obj and self.domain_obj.show_thumbnail

    @is_image.setter
    def is_image(self, other):
        pass

    @property
    def shortlink(self):
        protocol = "https" if app.config["FORCE_HTTPS"] else "http"

        if app.config["SHORT_DOMAIN"]:
            return f"{protocol}://{app.config['SHORT_DOMAIN']}/{self.base36id}"
        else:
            return f"{protocol}://{app.config['SERVER_NAME']}/post/{self.base36id}"

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

    def update_scores(self):
        self.score_hot = self.rank_hot
        self.score_disputed = self.rank_fiery
        self.score_top = self.score
        self.score_activity = self.rank_activity
        self.score_best = self.rank_best

        self.scores_last_updated_utc = g.timestamp


# class SaveRelationship(Base, Stndrd):

#     __tablename__="save_relationship"

#     id=Column(Integer, primary_key=True)
#     user_id=Column(Integer, ForeignKey("users.id"))
#     submission_id=Column(Integer, ForeignKey("submissions.id"))
from werkzeug.security import generate_password_hash, check_password_hash
from flask import g, session, abort, request
from time import strftime, gmtime
from sqlalchemy import (
    Column,
    Integer,
    BigInteger,
    String,
    Boolean,
    ForeignKey,
    FetchedValue,
    Index,
    and_,
    or_,
    select,
    func,
)
from sqlalchemy.orm import (
    relationship,
    deferred,
    joinedload,
    lazyload,
    contains_eager,
    aliased,
    Load,
    load_only,
)
from os import environ
from secrets import token_hex
from pyotp import TOTP
from mistletoe import markdown
import threading

from apps.helpers.base36 import base36encode
from apps.helpers.security import generate_hash, validate_hash
from apps.helpers.lazy import lazy
from apps.helpers.user_imports import send_notif
import apps.helpers.aws as aws
from apps.helpers.discord import add_role, delete_role, discord_log_event
from .alts import Alt
from .titles import TITLES
from .submission import Submission, SubmissionAux  # , SaveRelationship
from .comment import Comment, CommentAux, Notification
from .boards import Board
from .board_relationships import (
    ModRelationship,
    BanRelationship,
    ContributorRelationship,
    BoardBlock,
)
from .mix_ins import *
from .subscriptions import Subscription, Follow
from .userblock import UserBlock
from .badges import *
from .clients import *
from .paypal import PayPalTxn
from .flags import Report
from .votes import Vote

from apps.__main__ import Base, cache, app, g, db_session, debug


class User(Base, standard_mixin, age_mixin):
    __tablename__ = "users"

    # basic stuff
    id = Column(Integer, primary_key=True)
    username = Column(String, default=None)
    email = Column(String, default=None)
    passhash = deferred(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)
    bio = Column(String, default="")
    bio_html = Column(String, default="")
    real_id = Column(String, default=None)
    referred_by = Column(Integer, default=None)
    is_deleted = Column(Boolean, default=False)
    delete_reason = Column(String(500), default="")
    creation_region = Column(String(2), default=None)

    # ban & admin actions
    is_banned = Column(Integer, default=0)
    unban_utc = Column(Integer, default=0)
    ban_reason = Column(String, default="")
    ban_evade = Column(Integer, default=0)

    # content preferences
    defaultsorting = Column(String, default="hot")
    defaulttime = Column(String, default="all")
    hide_offensive = Column(Boolean, default=True)
    hide_bot = Column(Boolean, default=False)
    show_nsfl = Column(Boolean, default=False)
    custom_filter_list = Column(String(1000), default="")
    filter_nsfw = Column(Boolean, default=False)
    per_page_preference = Column(Integer, default=25)

    # security
    login_nonce = Column(Integer, default=0)
    mfa_secret = deferred(Column(String(64), default=None))
    is_private = Column(Boolean, default=False)
    is_nofollow = Column(Boolean, default=False)

    # profile
    title_id = Column(Integer)
    has_profile = Column(Boolean, default=False)
    has_banner = Column(Boolean, default=False)
    reserved = Column(String(256), default=None)
    is_nsfw = Column(Boolean, default=False)
    profile_nonce = Column(Integer, default=0)
    banner_nonce = Column(Integer, default=0)
    profile_upload_ip = deferred(Column(String(255), default=None))
    banner_upload_ip = deferred(Column(String(255), default=None))
    profile_upload_region = deferred(Column(String(2)))
    banner_upload_region = deferred(Column(String(2)))
    original_username = deferred(Column(String(255)))
    name_changed_utc = deferred(Column(Integer, default=0))

    # siege
    last_siege_utc = Column(Integer, default=0)

    # stored values from db-side functions
    stored_karma = Column(Integer, default=0)
    stored_subscriber_count = Column(Integer, default=0)

    # premium
    coin_balance = Column(Integer, default=0)
    premium_expires_utc = Column(Integer, default=0)
    negative_balance_cents = Column(Integer, default=0)

    # discord
    discord_id = Column(String(64), default=None)

    # color=Column(String(6), default="805ad5")
    # secondary_color=Column(String(6), default="ffff00")
    # signature=Column(String(280), default='')
    # signature_html=Column(String(512), default="")

    ## === RELATIONSHIPS ===

    # Content
    submissions = relationship(
        "Submission", lazy="dynamic", primaryjoin="Submission.author_id==User.id"
    )
    comments = relationship(
        "Comment", lazy="dynamic", primaryjoin="Comment.author_id==User.id"
    )
    notifications = relationship("Notification")

    # profile
    _badges = relationship("Badge", lazy="dynamic", backref="user")

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

    # Inter-user relationships
    following = relationship("Follow", primaryjoin="Follow.user_id==User.id")
    followers = relationship("Follow", primaryjoin="Follow.target_id==User.id")

    blocking = relationship(
        "UserBlock", lazy="dynamic", primaryjoin="User.id==UserBlock.user_id"
    )
    blocked = relationship(
        "UserBlock", lazy="dynamic", primaryjoin="User.id==UserBlock.target_id"
    )

    # apps
    _applications = relationship("OauthApp", lazy="dynamic")
    authorizations = relationship("ClientAuth", lazy="dynamic")

    # 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()))

    __table_args__ = (
        Index(
            "users_username_trgm_idx",
            "username",
            postgresql_using="gin",
            postgresql_ops={"username": "gin_trgm_ops"},
        ),
        Index(
            "users_original_username_trgm_idx",
            "original_username",
            postgresql_using="gin",
            postgresql_ops={"original_username": "gin_trgm_ops"},
        ),
        Index(
            "users_email_trgm_idx",
            "email",
            postgresql_using="gin",
            postgresql_ops={"email": "gin_trgm_ops"},
        ),
        Index(
            "users_created_utc_idx",
            "created_utc",
            postgresql_using="btree",
            postgresql_ops={"created_utc": "DESC"},
        ),
    )

    def __init__(self, **kwargs):
        if "password" in kwargs:
            kwargs["passhash"] = self.hash_password(kwargs["password"])
            kwargs.pop("password")

        if "created_utc" not in kwargs:
            kwargs["created_utc"] = g.timestamp

        super().__init__(**kwargs)

    def has_block(self, target):
        return (
            g.db.query(UserBlock)
            .filter_by(user_id=self.id, target_id=target.id)
            .first()
        )

    def is_blocked_by(self, user):
        return (
            g.db.query(UserBlock).filter_by(user_id=user.id, target_id=self.id).first()
        )

    def any_block_exists(self, other):
        return (
            g.db.query(UserBlock)
            .filter(
                or_(
                    and_(UserBlock.user_id == self.id, UserBlock.target_id == other.id),
                    and_(UserBlock.user_id == other.id, UserBlock.target_id == self.id),
                )
            )
            .first()
        )

    def has_blocked_guild(self, board):
        return (
            g.db.query(BoardBlock).filter_by(user_id=self.id, board_id=board.id).first()
        )

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

    @property
    def mfa_removal_code(self):
        hashstr = f"{self.mfa_secret}+{self.id}+{self.original_username}"

        hashstr = generate_hash(hashstr)

        removal_code = base36encode(int(hashstr, 16) % int("z" * 25, 36))

        # should be 25char long, left pad if needed
        while len(removal_code) < 25:
            removal_code = "0" + removal_code

        return removal_code

    @property
    def boards_subscribed(self):
        boards = [
            x.board for x in self.subscriptions if x.is_active and not x.board.is_banned
        ]
        return boards

    @property
    def age(self):
        return g.timestamp - self.created_utc

    @property
    def title(self):
        return TITLES.get(self.title_id)

    @cache.memoize(300)
    def recommended_list(self, page=1, per_page=25, filter_words=[], **kwargs):
        # get N most recent upvotes
        # get those posts
        # get a list of all users who also upvoted those things
        # get their upvotes
        # get those posts ordered by number of upvotes among those users
        # eliminate content based on personal filters

        # hard check to prevent this feature from being used as a "vote stalker"
        # Recommendations must be based on 4 other co-voting users minimum
        user_count = (
            g.db.query(Vote.user_id)
            .filter(
                Vote.vote_type == 1,
                Vote.user_id.in_(
                    select(Vote.user_id).filter(
                        Vote.vote_type == 1,
                        Vote.submission_id.in_(
                            select(Vote.submission_id)
                            .filter(Vote.vote_type == 1, Vote.user_id == self.id)
                            .order_by(Vote.created_utc.desc())
                            .limit(50)
                        ),
                    )
                ),
            )
            .distinct()
            .count()
        )

        if user_count < 4:
            return []

        # select post IDs, with global restrictions - no deleted, removed, or front-page-sticky content
        posts = (
            g.db.query(
                Submission.id,
                Submission.author_id,
                Submission.board_id,
                Submission.created_utc,
            )
            .options(
                load_only(
                    Submission.id,
                    Submission.author_id,
                    Submission.board_id,
                    Submission.created_utc,
                ),
                lazyload("*"),
            )
            .filter_by(is_banned=False, deleted_utc=0, stickied=False)
        )

        # filter out anything that's yours,
        # or that you've already voted on,
        # or that's too old

        posts = posts.filter(
            Submission.author_id != self.id,
            Submission.created_utc > g.timestamp - 2592000,
            Submission.id.notin_(
                select(Vote.submission_id).filter(
                    Vote.user_id == self.id, Vote.vote_type != 0
                )
            ),
        )

        # no nsfw content if personal settings dicate
        if self.filter_nsfw or not self.over_18:
            posts = posts.filter_by(over_18=False)

        # no racial slur content if personal settings dictate
        if self.hide_offensive:
            posts = posts.filter_by(is_offensive=False)

        # no bot content if personal settings dictate
        if self.hide_bot:
            posts = posts.filter_by(is_bot=False)

        # no disturbing/gruesome content if personal settings dictate
        if not self.show_nsfl:
            posts = posts.filter_by(is_nsfl=False)

        # no content from guilds on user's personal block list
        posts = posts.filter(
            Submission.board_id.notin_(
                select(BoardBlock.board_id).filter_by(user_id=g.user.id)
            )
        )

        # subquery - guilds where user is mod or has mod invite
        m = select(ModRelationship.board_id).filter_by(
            user_id=self.id, invite_rescinded=False
        )

        # subquery - guilds where user is added as contributor
        c = select(ContributorRelationship.board_id).filter_by(user_id=self.id)

        # no content from private guilds, unless the post was made while guild was public,
        # or the user is mod, or has mod invite, or is contributor, or is the post author
        posts = posts.filter(
            or_(
                # Submission.author_id == self.id,
                Submission.post_public == True,
                Submission.board_id.in_(m),
                Submission.board_id.in_(c),
            )
        )

        # subquery - other users who you are blocking
        blocking = select(UserBlock.target_id).filter_by(user_id=self.id)

        # subquery - other users you are blocked by
        blocked = select(UserBlock.user_id).filter_by(target_id=self.id)

        # no content where you're blocking the user or they're blocking you
        posts = posts.filter(
            Submission.author_id.notin_(blocking), Submission.author_id.notin_(blocked)
        )

        # guild-based restrictions on recommended content
        # no content from banned guilds, and no content from opt-out guilds unless the user is sub'd
        board_ids = select(Board.id).filter(
            Board.is_banned == False,
            or_(
                Board.all_opt_out == False,
                Submission.board_id.in_(
                    select(Subscription.board_id).filter_by(
                        user_id=g.user.id, is_active=True
                    )
                ),
            ),
        )
        posts = posts.filter(Submission.board_id.in_(board_ids))

        # personal filter word restrictions
        # no content whose title contains a personal filter word
        if filter_words:
            posts = posts.join(Submission.submission_aux)
            for word in filter_words:
                posts = posts.filter(not_(SubmissionAux.title.ilike(f"%{word}%")))

        ### Algorithm core part 1

        # read this code inside -> outside
        # select your 50 most recent upvoted posts,
        # then select the users who also upvoted those posts ("co-voting users")
        # then select the other stuff they upvoted
        # filter out any posts not in that list

        posts = posts.filter(
            Submission.id.in_(
                select(Vote.submission_id).filter(
                    Vote.vote_type == 1,
                    Vote.user_id.in_(
                        select(Vote.user_id).filter(
                            Vote.vote_type == 1,
                            Vote.submission_id.in_(
                                select(Vote.submission_id)
                                .filter(Vote.vote_type == 1, Vote.user_id == self.id)
                                .order_by(Vote.created_utc.desc())
                                .limit(50)
                            ),
                        )
                    ),
                )
            )
        )

        # at this point we have an unfinished query representing the pool of all possible For You posts
        # The next steps involve some heavy lifting on the part of the database
        # To save computation resources in the next steps, sort by Top and cut off everything below the first 500

        # this is now an unsent db query that can be used in subsequent steps
        posts_subq = posts.order_by(Submission.score_top.desc()).limit(500).subquery()

        # Votes subquery - the only votes we care about are those from users who co-voted the user's last 100 upvotes
        # this is similar to the algoritm core part 1, with an added filter of only paying attention to votes on the final pool of 500
        votes = (
            select(Vote)
            .options(lazyload("*"))
            .filter(
                Vote.vote_type == 1,
                Vote.user_id.in_(
                    select(Vote.user_id).filter(
                        Vote.vote_type == 1,
                        Vote.submission_id.in_(
                            select(Vote.submission_id)
                            .filter(Vote.vote_type == 1, Vote.user_id == self.id)
                            .order_by(Vote.created_utc.desc())
                            .limit(50)
                        ),
                    )
                ),
                Vote.submission_id.in_(select(posts_subq.c.id)),
            )
        )

        # This assigns posts their initial score - the number of upvotes it has from co-voting users
        starting_rank = func.count(votes.c.submission_id).label("rank")
        vote_scores = (
            g.db.query(votes.c.submission_id.label("id"), starting_rank)
            .group_by(votes.c.submission_id)
            .order_by(starting_rank.desc())
            .subquery()
        )

        # create final scoring matrix, starting with post id, author_id, and board_id,
        # join it with the vote scores above
        # and add in penalty columns based on age and prior entries of the same author/board (promoting variety)

        age_penalty = (
            (g.timestamp - posts_subq.c.created_utc) // (60 * 60 * 24 * 2)
        ).label("age_penalty")
        scores = (
            g.db.query(
                posts_subq.c.id,
                posts_subq.c.author_id,
                posts_subq.c.board_id,
                posts_subq.c.created_utc,
                vote_scores.c.rank,
                age_penalty,
                func.row_number()
                .over(
                    partition_by=posts_subq.c.author_id,
                    order_by=(vote_scores.c.rank - age_penalty).desc(),
                )
                .label("user_penalty"),
                func.row_number()
                .over(
                    partition_by=posts_subq.c.board_id,
                    order_by=(vote_scores.c.rank - age_penalty).desc(),
                )
                .label("board_penalty"),
            )
            .join(vote_scores, posts_subq.c.id == vote_scores.c.id)
            .subquery()
        )

        # everything so far has been for the purpose of putting all candidate entries
        # into an ephemeral score table, which has columns:
        # id | author_id | board_id | created_utc | vote_rank | age_penalty | user_penalty | board_penalty

        # Last step, get *just* the ids, sorted by (vote_rank - age_penalty*a - user_penalty-b - board_penalty*c)

        post_ids = g.db.query(scores.c.id).order_by(
            # Submission.score_best.desc()
            # scores.c.rank.desc()
            (
                scores.c.rank
                - scores.c.user_penalty
                - scores.c.board_penalty * 2
                - scores.c.age_penalty
            ).desc(),
            scores.c.created_utc.desc(),
        )

        # and paginate
        post_ids = post_ids.offset(per_page * (page - 1)).limit(per_page + 1).all()

        return [x.id for x in post_ids]

    @cache.memoize()
    def idlist(self, sort=None, page=1, t=None, filter_words=[], per_page=25, **kwargs):
        posts = (
            g.db.query(Submission)
            .options(load_only(Submission.id), lazyload("*"))
            .filter_by(is_banned=False, deleted_utc=0, stickied=False)
        )

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

        if self.hide_offensive:
            posts = posts.filter_by(is_offensive=False)

        if self.hide_bot:
            posts = posts.filter_by(is_bot=False)

        if not self.show_nsfl:
            posts = posts.filter_by(is_nsfl=False)

        board_ids = select(Subscription.board_id).filter_by(
            user_id=self.id, is_active=True
        )
        user_ids = (
            select(Follow.user_id)
            .filter_by(user_id=self.id)
            .join(Follow.target)
            .filter(User.is_private == False, User.is_nofollow == False)
        )

        posts = posts.filter(
            or_(Submission.board_id.in_(board_ids), Submission.author_id.in_(user_ids))
        )

        if self.admin_level < 4:
            # admins can see everything

            m = select(ModRelationship.board_id).filter_by(
                user_id=self.id, invite_rescinded=False
            )
            c = select(ContributorRelationship.board_id).filter_by(user_id=self.id)
            posts = posts.filter(
                or_(
                    Submission.author_id == self.id,
                    Submission.post_public == True,
                    Submission.board_id.in_(m),
                    Submission.board_id.in_(c),
                )
            )

            blocking = select(UserBlock.target_id).filter_by(user_id=self.id)
            blocked = select(UserBlock.user_id).filter_by(target_id=self.id)

            posts = (
                posts.filter(
                    Submission.author_id.notin_(blocking)  # ,
                    # Submission.author_id.notin_(blocked)
                )
                .join(Submission.board)
                .filter(Board.is_banned == False)
            )

        if filter_words:
            posts = posts.join(Submission.submission_aux)
            for word in filter_words:
                posts = posts.filter(not_(SubmissionAux.title.ilike(f"%{word}%")))

        if t:
            now = g.timestamp
            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)

        gt = kwargs.get("gt")
        lt = kwargs.get("lt")

        if gt:
            posts = posts.filter(Submission.created_utc > gt)

        if lt:
            posts = posts.filter(Submission.created_utc < lt)

        if sort == None:
            sort = self.defaultsorting or "hot"

        if sort == "hot":
            posts = posts.order_by(Submission.score_best.desc())
        elif sort == "new":
            posts = posts.order_by(Submission.created_utc.desc())
        elif sort == "old":
            posts = posts.order_by(Submission.created_utc.asc())
        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)

        return [
            x.id for x in posts.offset(per_page * (page - 1)).limit(per_page + 1).all()
        ]

    @cache.memoize()
    def userpagelisting(self, page=1, sort="new", t="all", per_page=25):
        submissions = (
            g.db.query(Submission)
            .options(load_only(Submission.id))
            .filter_by(author_id=self.id)
        )

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

        if g.user and g.user.hide_offensive and g.user.id != self.id:
            submissions = submissions.filter_by(is_offensive=False)

        if not (g.user and g.user.admin_level >= 3):
            submissions = (
                submissions.filter_by(deleted_utc=0, is_banned=False)
                .join(Submission.board)
                .filter(Board.is_banned == False)
            )

        if g.user and g.user.admin_level >= 4:
            pass
        elif g.user:
            m = select(ModRelationship.board_id).filter_by(
                user_id=g.user.id, invite_rescinded=False
            )
            c = select(ContributorRelationship.board_id).filter_by(user_id=g.user.id)
            submissions = submissions.filter(
                or_(
                    Submission.author_id == g.user.id,
                    Submission.post_public == True,
                    Submission.board_id.in_(m),
                    Submission.board_id.in_(c),
                )
            )
        else:
            submissions = submissions.filter(Submission.post_public == True)

        if sort == "hot":
            submissions = submissions.order_by(Submission.score_best.desc())
        elif sort == "new":
            submissions = submissions.order_by(Submission.created_utc.desc())
        elif sort == "old":
            submissions = submissions.order_by(Submission.created_utc.asc())
        elif sort == "disputed":
            submissions = submissions.order_by(Submission.score_disputed.desc())
        elif sort == "top":
            submissions = submissions.order_by(Submission.score_top.desc())
        elif sort == "activity":
            submissions = submissions.order_by(Submission.score_activity.desc())

        now = g.timestamp
        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
        submissions = submissions.filter(Submission.created_utc >= cutoff)

        listing = [
            x.id for x in submissions.offset(per_page * (page - 1)).limit(per_page + 1)
        ]
        return listing

    @cache.memoize()
    def commentlisting(self, page=1, sort="new", t="all", per_page=25):
        comments = (
            self.comments.options(load_only(Comment.id))
            .filter(Comment.parent_submission is not None)
            .join(Comment.post)
        )

        if not (g.user and g.user.over_18):
            comments = comments.filter(Submission.over_18 == False)

        if g.user and g.user.hide_offensive and g.user.id != self.id:
            comments = comments.filter(Comment.is_offensive == False)

        if g.user and not g.user.show_nsfl:
            comments = comments.filter(Submission.is_nsfl == False)

        if not (g.user and g.user.admin_level >= 3):
            comments = comments.filter(
                Comment.deleted_utc == 0, Comment.is_banned == False
            )

        if g.user and g.user.admin_level >= 4:
            pass
        elif g.user:
            m = select(ModRelationship).filter_by(
                user_id=g.user.id, invite_rescinded=False
            )
            c = select(ContributorRelationship).filter_by(user_id=g.user.id)

            comments = (
                comments.join(m, m.c.board_id == Submission.board_id, isouter=True)
                .join(c, c.c.board_id == Submission.board_id, isouter=True)
                .join(Board, Board.id == Submission.board_id)
            )

            comments = comments.filter(
                or_(
                    Comment.author_id == g.user.id,
                    Submission.post_public == True,
                    Board.is_private == False,
                    m.c.board_id != None,
                    c.c.board_id != None,
                ),
                Board.is_banned == False,
            )
        else:
            comments = comments.join(Board, Board.id == Submission.board_id).filter(
                or_(Submission.post_public == True, Board.is_private == False),
                Board.is_banned == False,
            )

        comments = comments.options(contains_eager(Comment.post))

        if sort == "hot":
            comments = comments.order_by(Comment.score_hot.desc())
        elif sort == "new":
            comments = comments.order_by(Comment.created_utc.desc())
        elif sort == "old":
            comments = comments.order_by(Comment.created_utc.asc())
        elif sort == "disputed":
            comments = comments.order_by(Comment.score_disputed.desc())
        elif sort == "top":
            comments = comments.order_by(Comment.score_top.desc())

        now = g.timestamp
        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
        comments = comments.filter(Comment.created_utc >= cutoff)

        comments = comments.offset(per_page * (page - 1)).limit(per_page + 1)

        listing = [c.id for c in comments]
        return listing

    @property
    @lazy
    def mods_anything(self):
        return bool([i for i in self.moderates if i.accepted])

    @property
    @lazy
    def subscribed_to_anything(self):
        return bool([i for i in self.subscriptions if i.is_active])

    @property
    def boards_modded(self):
        z = [
            x.board
            for x in self.moderates
            if x and x.board and x.accepted and not x.board.is_banned
        ]
        z = sorted(z, key=lambda x: x.name)

        return z

    @property
    @cache.memoize()  # 1hr cache time for user rep
    def karma(self):
        energy = self.energy if self.energy is not None else 0
        post_count = self.post_count if self.post_count is not None else 0
        return energy - post_count

    @property
    @cache.memoize()
    def comment_karma(self):
        if self.comment_energy is None or self.comment_count is None:
            comment_energy = (
                self.comment_energy if self.comment_energy is not None else 0
            )
            comment_count = self.comment_count if self.comment_count is not None else 0
        return comment_energy - comment_count

    @property
    @cache.memoize()
    def true_score(self):
        return max((self.karma + self.comment_karma), -5)

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

    @property
    @cache.memoize()
    @lazy
    def has_report_queue(self):
        board_ids = (
            select(ModRelationship.board_id)
            .options(lazyload("*"))
            .filter(
                ModRelationship.user_id == self.id,
                ModRelationship.accepted == True,
                or_(
                    ModRelationship.perm_full == True,
                    ModRelationship.perm_content == True,
                ),
            )
        )

        posts = (
            g.db.query(Submission)
            .options(lazyload("*"))
            .filter(
                Submission.board_id.in_(board_ids),
                Submission.mod_approved == None,
                Submission.is_banned == False,
                Submission.deleted_utc == 0,
            )
            .join(Report, Report.post_id == Submission.id)
        )

        if not self.over_18:
            posts = posts.filter(Submission.over_18 == False)

        return bool(posts.first())

    @property
    def banned_by(self):
        if not self.is_banned:
            return None

        return g.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):
        return post.voted

    def vote_status_on_comment(self, comment):
        return comment.voted

    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)

    @property
    def feedkey(self):
        return generate_hash(
            f"{self.username}{self.id}{self.feed_nonce}{self.created_utc}"
        )

    @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
    def uid_permalink(self):
        return f"/uid/{self.base36id}"

    @property
    def original_link(self):
        return f"/@{self.original_username}"

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

    def notification_commentlisting(
        self,
        page=1,
        all_=False,
        replies_only=False,
        mentions_only=False,
        system_only=False,
    ):
        notifications = (
            g.db.query(Notification)
            .options(
                lazyload("*"),
            )
            .join(Notification.comment)
            .filter(
                Notification.user_id == self.id,
                Comment.is_banned == False,
                Comment.deleted_utc == 0,
            )
        )

        if replies_only:
            cs = select(Comment.id).filter(Comment.author_id == self.id)
            ps = select(Submission.id).filter(Submission.author_id == self.id)
            notifications = notifications.filter(
                or_(
                    Comment.parent_comment_id.in_(cs),
                    and_(Comment.level == 1, Comment.parent_submission.in_(ps)),
                )
            )

        elif mentions_only:
            cs = select(Comment.id).filter(Comment.author_id == self.id)
            ps = select(Submission.id).filter(Submission.author_id == self.id)
            notifications = notifications.filter(
                and_(
                    Comment.parent_comment_id.notin_(cs),
                    or_(Comment.level > 1, Comment.parent_submission.notin_(ps)),
                )
            )
        elif system_only:
            notifications = notifications.filter(Comment.author_id == 1)

        elif not all_:
            notifications = notifications.filter(Notification.read == False)

        notifications = notifications.options(contains_eager(Notification.comment))

        notifications = (
            notifications.order_by(
                # staying at 25 for performance reasons
                Notification.id.desc()
            )
            .offset(25 * (page - 1))
            .limit(26)
            .all()
        )

        mark_as_read = False
        for x in notifications[0:25]:
            if x.read:
                continue

            x.read = True
            g.db.add(x)
            mark_as_read = True

        if mark_as_read:
            g.db.commit()

        return [x.comment_id for x in notifications]

    def notification_postlisting(self, all_=False, page=1):
        notifications = (
            g.db.query(Notification)
            .options(lazyload("*"))
            .join(Notification.post)
            .filter(
                Notification.user_id == self.id,
                Submission.is_banned == False,
                Submission.deleted_utc == 0,
            )
        )

        if not all_:
            notifications = notifications.filter(Notification.read == False)

        notifications = (
            notifications.options(contains_eager(Notification.post))
            .order_by(Notification.id.desc())
            .offset(25 * (page - 1))
            .limit(26)
        )

        mark_as_read = False
        for x in notifications[0:25]:
            if x.read:
                continue

            x.read = True
            g.db.add(x)
            mark_as_ready = True

        if mark_as_read:
            g.db.commit()

        g.db.commit()
        return [x.submission_id for x in notifications]

    @property
    @lazy
    def mentions_count(self):
        cs = select(Comment.id).filter(Comment.author_id == self.id)
        ps = select(Submission.id).filter(Submission.author_id == self.id)
        return (
            self.notifications.options(lazyload("*"))
            .join(Notification.comment)
            .filter(
                Notification.read == False,
                Comment.is_banned == False,
                Comment.deleted_utc == 0,
            )
            .filter(
                and_(
                    Comment.parent_comment_id.notin_(cs),
                    or_(Comment.level > 1, Comment.parent_submission.notin_(ps)),
                )
            )
            .count()
        )

    @property
    @lazy
    def replies_count(self):
        cs = select(Comment.id).filter(Comment.author_id == self.id)
        ps = select(Submission.id).filter(Submission.author_id == self.id)
        return (
            self.notifications.options(lazyload("*"))
            .join(Notification.comment)
            .filter(Comment.is_banned == False, Comment.deleted_utc == 0)
            .filter(
                Notification.read == False,
                or_(
                    Comment.parent_comment_id.in_(cs),
                    and_(Comment.level == 1, Comment.parent_submission.in_(ps)),
                ),
            )
            .count()
        )

    @property
    @lazy
    def post_notifications_count(self):
        return (
            g.db.query(Notification)
            .filter(Notification.user_id == self.id, Notification.read == False)
            .join(Submission, Submission.id == Notification.submission_id)
            .filter(
                Submission.is_banned == False,
                Submission.deleted_utc == 0,
            )
            .count()
        )

    @property
    @lazy
    def system_notif_count(self):
        return (
            g.db.query(Notification)
            .options(lazyload("*"))
            .join(Notification.comment)
            .filter(
                Notification.user_id == self.id,
                Notification.read == False,
                Comment.author_id == 1,
            )
            .count()
        )

    @property
    @lazy
    def notifications_count(self):
        return (
            g.db.query(Notification)
            .options(lazyload("*"))
            .filter(Notification.user_id == self.id, Notification.read == False)
            .join(Notification.comment, isouter=True)
            .join(Notification.post, isouter=True)
            .filter(
                or_(
                    and_(Comment.is_banned == False, Comment.deleted_utc == 0),
                    and_(Submission.is_banned == False, Submission.deleted_utc == 0),
                )
            )
            .count()
        )

    @property
    def post_count(self):
        return self.submissions.filter_by(is_banned=False).count()

    @property
    def comment_count(self):
        return (
            self.comments.filter(Comment.parent_submission != None)
            .filter_by(is_banned=False, deleted_utc=0)
            .count()
        )

    @property
    @lazy
    def alts(self):
        subq = (
            g.db.query(Alt)
            .filter(or_(Alt.user1 == self.id, Alt.user2 == self.id))
            .subquery()
        )

        data = (
            g.db.query(User, aliased(Alt, alias=subq))
            .join(subq, or_(subq.c.user1 == User.id, subq.c.user2 == User.id))
            .filter(User.id != self.id)
            .order_by(User.username.asc())
            .all()
        )

        data = [x for x in data]
        output = []
        for x in data:
            user = x[0]
            user._is_manual = x[1].is_manual
            output.append(user)

        return output

    def alts_subquery(self):
        returnselect(User.id).filter(
            or_(
                User.id.in_(select(Alt.user1).filter(Alt.user2 == self.id)),
                User.id.in_(select(Alt.user2).filter(Alt.user1 == self.id)),
            )
        )

    def alts_threaded(self, db):
        subq = select(Alt).filter(or_(Alt.user1 == self.id, Alt.user2 == self.id))

        data = (
            db.query(User, aliased(Alt, alias=subq))
            .join(subq, or_(subq.c.user1 == User.id, subq.c.user2 == User.id))
            .filter(User.id != self.id)
            .order_by(User.username.asc())
            .all()
        )

        data = [x for x in data]
        output = []
        for x in data:
            user = x[0]
            user._is_manual = x[1].is_manual
            output.append(user)

        return output

    def has_follower(self, user):
        return g.db.query(Follow).filter_by(target_id=self.id, user_id=user.id).first()

    def set_profile(self, file):
        self.del_profile()
        self.profile_nonce += 1

        aws.upload_file(
            name=f"uid/{self.base36id}/profile-{self.profile_nonce}.png",
            file=file,
            resize=(100, 100),
        )
        self.has_profile = True
        self.profile_upload_ip = request.remote_addr
        self.profile_set_utc = g.timestamp
        self.profile_upload_region = request.headers.get("cf-ipcountry")
        g.db.add(self)
        g.db.commit()

    def set_banner(self, file):
        self.del_banner()
        self.banner_nonce += 1

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

        self.has_banner = True
        self.banner_upload_ip = request.remote_addr
        self.banner_upload_region = request.headers.get("cf-ipcountry")

        g.db.add(self)
        g.db.commit()

    def del_profile(self, db=None):
        aws.delete_file(name=f"uid/{self.base36id}/profile-{self.profile_nonce}.png")
        self.has_profile = False
        self.profile_nonce += 1
        if db:
            db.add(self)
            db.commit()
            db.close()
        else:
            g.db.add(self)
            g.db.commit()

    def del_banner(self, db=None):
        aws.delete_file(name=f"uid/{self.base36id}/banner-{self.banner_nonce}.png")
        self.has_banner = False
        self.banner_nonce += 1
        if db:
            db.add(self)
            db.commit()
            db.close()
        else:
            g.db.add(self)
            g.db.commit()

    @property
    def banner_url(self):
        if self.has_banner:
            return f"https://{app.config['S3_BUCKET_URL']}/uid/{self.base36id}/banner-{self.banner_nonce}.png"
        else:
            return app.config["IMG_URL_JUMBOTRON"]

    @property
    def dynamic_profile_url(self):
        return f"/uid/{self.base36id}/pic/profile/{self.profile_nonce}"

    @property
    def profile_url(self):
        if self.has_profile and not self.is_deleted:
            return f"https://{app.config['S3_BUCKET_URL']}/uid/{self.base36id}/profile-{self.profile_nonce}.png"
        else:
            return f"http{'s' if app.config['FORCE_HTTPS'] else ''}://{app.config['SERVER_NAME']}/logo/fontawesome/solid//{app.config['COLOR_PRIMARY']}/150"

    @property
    def can_make_guild(self):
        if app.config["GUILD_CREATION_REQ"] == -1:
            return self.admin_level >= 3

        elif app.config["GUILD_CREATION_REQ"] == 0:
            return self.can_join_gms

        return (
            self.has_premium
            or self.admin_level >= 3
            or self.true_score >= app.config["GUILD_CREATION_REQ"]
        ) and self.can_join_gms

    @property
    def can_join_gms(self):
        if app.config["MAX_GUILD_COUNT"] == 0:
            return True

        return (
            len([x for x in self.boards_modded if x.is_siegable])
            < app.config["MAX_GUILD_COUNT"]
        )

    @property
    def can_siege(self):
        if self.is_suspended:
            return False

        now = g.timestamp

        return now - max(self.last_siege_utc, self.created_utc) > 60 * 60 * 24 * 7

    @property
    def can_submit_image(self):
        # Has premium
        return (
            self.has_premium or self.true_score >= app.config["UPLOAD_IMAGE_REP"]
        ) and not g.is_tor

    @property
    def can_upload_avatar(self):
        return (
            self.has_premium or self.true_score >= app.config["PROFILE_UPLOAD_REP"]
        ) and not g.is_tor

    @property
    def can_upload_banner(self):
        return (
            self.has_premium or self.true_score >= app.config["BANNER_UPLOAD_REP"]
        ) and not g.is_tor

    @property
    def json_raw(self):
        data = {
            "username": self.username,
            "permalink": self.permalink,
            "is_banned": self.is_suspended,
            "is_premium": self.has_premium_no_renew,
            "created_utc": self.created_utc,
            "id": self.base36id,
            "is_private": self.is_private,
            "profile_url": self.profile_url,
            "banner_url": self.banner_url,
            "title": self.title.json if self.title else None,
            "bio": self.bio,
            "bio_html": self.bio_html,
        }

        if self.real_id:
            data["real_id"] = self.real_id

        return data

    @property
    def json_core(self):
        now = g.timestamp
        if self.is_suspended:
            return {
                "username": self.username,
                "permalink": self.permalink,
                "is_banned": True,
                "is_permanent_ban": not bool(self.unban_utc),
                "ban_reason": self.ban_reason,
                "id": self.base36id,
            }

        elif self.is_deleted:
            return {
                "username": self.username,
                "permalink": self.permalink,
                "is_deleted": True,
                "id": self.base36id,
            }
        return self.json_raw

    @property
    def json(self):
        data = self.json_core

        if self.is_suspended or self.is_deleted:
            return data

        data["badges"] = [x.json_core for x in self.badges]
        data["post_rep"] = int(self.karma)
        data["comment_rep"] = int(self.comment_karma)
        data["post_count"] = self.post_count
        data["comment_count"] = self.comment_count

        return data

    @property
    def total_karma(self):
        return 503 if self.id == 1 else max(self.karma + self.comment_karma, -5)

    @property
    def is_valid(self):
        if self.is_banned and self.unban_utc == 0:
            return False

        elif self.is_deleted:
            return False

        else:
            return True

    def ban(self, admin=None, reason=None, message=None, days=0):
        self.is_banned = admin.id if admin else 1

        if reason:
            self.ban_reason = reason

        g.db.add(self)
        g.db.flush()

        # send message

        text = "Your account has been"

        if not admin:
            text += " automatically"

        if days:
            text += f" suspended for {days} day{'s' if days>1 else ''}"
        else:
            text += " terminated"

        if reason:
            text += f" for the following reason:\n\n> {reason}"
        else:
            text += "."

        if not admin:
            text += f"\n\nBecause this ban was performed automatically, it may be appealed. If your ban was applied in error, [join the {app.config['SITE_NAME']} discord server](/discord), and you will be automatically added to the ban appeals channel."

        if message:
            text += f"\n\nAdditional private message from the admins:\n\n{message}"

        send_notif(self, text)

        if days > 0:
            ban_time = g.timestamp + (days * 86400)
            self.unban_utc = ban_time

        else:
            # Takes care of all functions needed for account termination
            self.unban_utc = 0
            if self.has_banner:
                thread1 = threading.Thread(
                    target=self.del_banner, kwargs={"db": db_session()}
                )
                thread1.start()
            if self.has_profile:
                thread2 = threading.Thread(
                    target=self.del_profile, kwargs={"db": db_session()}
                )
                thread2.start()

            add_role(self, "banned")
            delete_role(self, "member")

            # unprivate guilds if no mods remaining
            for b in self.boards_modded:
                if b.mods_count == 0:
                    b.is_private = False
                    b.restricted_posting = False
                    # b.all_opt_out = False
                    g.db.add(b)

            # ban api applications
            for application in self.applications:
                application.is_banned = True
                g.db.add(application)

            self.has_profile = False
            self.profile_nonce += 1
            self.has_banner = False
            self.banner_nonce += 1

        g.db.add(self)
        g.db.commit()

        discord_ban_action = f"{days} Day Ban" if days else "Perm Ban"
        discord_log_event(
            discord_ban_action, self, admin, reason=reason, admin_action=True
        )

    def unban(self):
        # Takes care of all functions needed for account reinstatement.

        self.is_banned = 0
        self.unban_utc = 0

        delete_role(self, "banned")
        add_role(self, "member")

        g.db.add(self)

        discord_log_event(
            "Unban", self, g.user, reason=self.ban_reason, admin_action=True
        )

        text = f'Your {app.config["SITE_NAME"]} account has been reinstated. Please review the [Terms of Service](/help/terms) and [Rules](/help/rules), and avoid breaking them in the future.'
        send_notif(self, text)

    @property
    def is_suspended(self):
        return self.is_banned and (self.unban_utc == 0 or self.unban_utc > g.timestamp)

    @property
    def is_permbanned(self):
        return self.is_banned and not self.unban_utc

    @property
    def is_blocking(self):
        return self.__dict__.get("_is_blocking", 0)

    @property
    def is_blocked(self):
        return self.__dict__.get("_is_blocked", 0)

    def refresh_selfset_badges(self):
        for badge in BADGE_DEFS.values():
            if not badge.__dict__.get("expr"):
                continue

            should_have = badge.evaluate(self)
            if should_have:
                if not self.has_badge(badge.id):
                    new_badge = Badge(
                        user_id=self.id, badge_id=badge.id, created_utc=g.timestamp
                    )
                    g.db.add(new_badge)

            elif should_have == False:
                bad_badge = self.has_badge(badge.id)
                if bad_badge:
                    g.db.delete(bad_badge)

        g.db.commit()

    @property
    def applications(self):
        return [x for x in self._applications.order_by(OauthApp.id.asc()).all()]

    # def saved_idlist(self, page=1, per_page=25):

    #     posts = g.db.query(Submission.id).options(lazyload('*')).filter_by(is_banned=False,
    #                                                                        deleted_utc=0
    #                                                                        )

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

    #     saved=select(SaveRelationship.submission_id).filter(SaveRelationship.user_id==self.id)
    #     posts=posts.filter(Submission.id.in_(saved))

    #     if self.admin_level < 4:
    #         # admins can see everything

    #         m = select(
    #             ModRelationship.board_id).filter_by(
    #             user_id=self.id,
    #             invite_rescinded=False)
    #         c = select(
    #             ContributorRelationship.board_id).filter_by(
    #             user_id=self.id)
    #         posts = posts.filter(
    #             or_(
    #                 Submission.author_id == self.id,
    #                 Submission.post_public == True,
    #                 Submission.board_id.in_(m),
    #                 Submission.board_id.in_(c)
    #             )
    #         )

    #         blocking = select(
    #             UserBlock.target_id).filter_by(
    #             user_id=self.id)
    #         blocked = select(
    #             UserBlock.user_id).filter_by(
    #             target_id=self.id)

    #         posts = posts.filter(
    #             Submission.author_id.notin_(blocking),
    #             Submission.author_id.notin_(blocked)
    #         )

    #     posts=posts.order_by(Submission.created_utc.desc())

    #     return [x[0] for x in posts.offset(per_page * (page - 1)).limit(per_page+1).all()]

    def guild_rep(self, guild, recent=0):
        posts = g.db.query(Submission.score_top).filter_by(
            is_banned=False, original_board_id=guild.id, is_bot=False
        )

        if recent:
            cutoff = g.timestamp - 60 * 60 * 24 * recent
            posts = posts.filter(Submission.created_utc > cutoff)

        posts = posts.all()

        post_rep = sum([x[0] for x in posts]) - len(posts)

        comments = g.db.query(Comment.score_top).filter_by(
            is_banned=False, original_board_id=guild.id, is_bot=False
        )

        if recent:
            cutoff = g.timestamp - 60 * 60 * 24 * recent
            comments = comments.filter(Comment.created_utc > cutoff)

        comments = comments.all()

        comment_rep = sum([x[0] for x in comments]) - len(comments)

        return int(post_rep + comment_rep)

    @property
    def has_premium(self):
        now = g.timestamp

        if self.negative_balance_cents:
            return False

        if self.is_permbanned:
            return False

        elif self.premium_expires_utc > now:
            return True

        elif self.coin_balance >= 1:
            self.coin_balance -= 1
            self.premium_expires_utc = now + 60 * 60 * 24 * 7

            add_role(self, "premium")

            g.db.add(self)

            return True

        else:
            if self.premium_expires_utc:
                delete_role(self, "premium")
                self.premium_expires_utc = 0
                g.db.add(self)

            return False

    @property
    def has_premium_no_renew(self):
        now = g.timestamp

        if self.negative_balance_cents:
            return False
        elif self.premium_expires_utc > now:
            return True
        elif self.coin_balance >= 1:
            return True
        else:
            return False

    @property
    def renew_premium_time(self):
        return strftime("%d %b %Y at %H:%M:%S", gmtime(self.premium_expires_utc))

    @property
    def filter_words(self):
        l = (
            [i.lstrip().rstrip() for i in self.custom_filter_list.split("\n")]
            if self.custom_filter_list
            else []
        )
        l = [i for i in l if i]
        return l

    @property
    def boards_modded_ids(self):
        return [x.id for x in self.boards_modded]

    @property
    def txn_history(self):
        return (
            self._transactions.filter(PayPalTxn.status != 1)
            .order_by(PayPalTxn.created_utc.desc())
            .all()
        )

    @property
    def json_admin(self):
        data = self.json_raw

        data["creation_ip"] = self.creation_ip
        data["creation_region"] = self.creation_region
        data["email"] = self.email
        data["email_verified"] = self.is_activated

        return data

    @property
    def can_upload_comment_image(self):
        return self.has_premium and not g.is_tor

    @property
    def can_change_name(self):
        return (
            self.name_changed_utc
            < g.timestamp - 60 * 60 * 24 * app.config["COOLDOWN_DAYS_CHANGE_USERNAME"]
            and self.coin_balance >= app.config["COINS_REQUIRED_CHANGE_USERNAME"]
        )

    @cache.memoize(60 * 60 * 24)
    def badges_function(self):
        self.refresh_selfset_badges()
        return self._badges.all()

    badges = property(badges_function)

    @property
    def is_following(self):
        return self.__dict__.get("_is_following", None)

    @property
    def unban_string(self):
        if self.unban_utc == 0:
            return "Permanent Ban"

        wait = self.unban_utc - g.timestamp

        if wait < 60:
            text = "just a moment"
        else:
            days = wait // (60 * 60 * 24)
            wait -= days * 60 * 60 * 24

            hours = wait // (60 * 60)
            wait -= hours * 60 * 60

            minutes = wait // 60

            text = f"{days}d {hours:02d}h {minutes:02d}m"

        return f"Unban in {text}"
from flask import g, render_template, request
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, FetchedValue, Index, Float
from sqlalchemy.orm import relationship, deferred
from sqlalchemy.ext.associationproxy import association_proxy
from random import randint
import math

from apps.helpers.base36 import base36encode
from apps.helpers.lazy import lazy

from .mix_ins import *
from .votes import CommentVote
from .flags import CommentFlag
from .badwords import BadWord

from apps.__main__ import Base, cache, debug


class CommentAux(Base):

    __tablename__ = "comments_aux"

    key_id = Column(Integer, primary_key=True)
    id = Column(Integer, ForeignKey("comments.id"), index=True, unique=True)
    body = Column(String(10000), default=None)
    body_html = Column(String(20000))
    ban_reason = Column(String(256), default='')

    __table_args__=(
        Index(
            "comments_aux_body_trgm_idx", "body",
            postgresql_using="gin",
            postgresql_ops={
                'body':'gin_trgm_ops'
                }
            ),
        )


class Comment(Base, standard_mixin, age_mixin, score_mixin, fuzzing_mixin):

    __tablename__ = "comments"

    id = Column(Integer, primary_key=True)
    comment_aux = relationship(
        "CommentAux",
        lazy="joined",
        uselist=False,
        innerjoin=True,
        primaryjoin="Comment.id==CommentAux.id")
    author_id = Column(Integer, ForeignKey("users.id"), index=True)
    parent_submission = Column(Integer, ForeignKey("submissions.id"), index=True)

    # this column is foreignkeyed to comment(id) but we can't do that yet as
    # "comment" class isn't yet defined
    #parent_fullname = Column(Integer)

    created_utc = Column(Integer, default=0)
    edited_utc = Column(Integer, default=0)
    is_banned = Column(Boolean, default=False)
    distinguish_level = Column(Integer, default=0)
    gm_distinguish = Column(Integer, ForeignKey("boards.id"), default=None)
    distinguished_board = relationship("Board", lazy="joined", primaryjoin="Comment.gm_distinguish==Board.id")
    deleted_utc = Column(Integer, default=0)
    purged_utc = Column(Integer, default=0)
    is_approved = Column(Integer, default=0)
    approved_utc = Column(Integer, default=0)
    creation_ip = Column(String(64), default='')
    score_disputed = Column(Float, default=0)
    score_hot = Column(Float, default=0)
    score_top = Column(Integer, default=1)
    level = Column(Integer, default=0)
    parent_comment_id = Column(Integer, ForeignKey("comments.id"), index=True)
    original_board_id = Column(Integer, ForeignKey("boards.id"), index=True)

    over_18 = Column(Boolean, default=False)
    is_offensive = Column(Boolean, default=False)
    is_nsfl = Column(Boolean, default=False)
    is_bot = Column(Boolean, default=False)
    is_pinned = Column(Boolean, default=False)
    creation_region=Column(String(2), default=None)

    app_id = Column(Integer, ForeignKey("oauth_apps.id"), default=None)
    oauth_app=relationship("OauthApp")

    post = relationship("Submission", backref="comments")
    flags = relationship("CommentFlag", backref="comment")
    author = relationship(
        "User",
        lazy="joined",
        innerjoin=True,
        primaryjoin="User.id==Comment.author_id",
        viewonly=True)
    board = association_proxy("post", "board")
    original_board = relationship(
        "Board", primaryjoin="Board.id==Comment.original_board_id")

    upvotes = Column(Integer, default=1, nullable=False)
    downvotes = Column(Integer, default=0, nullable=False)

    parent_comment = relationship("Comment", remote_side=[id], viewonly=True)
    child_comments = relationship("Comment", remote_side=[parent_comment_id], viewonly=True)

    awards = relationship("AwardRelationship", lazy="joined")

    # 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()))
    is_public = deferred(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()))

    __table_args__=(
        Index(
            "comments_score_hot_desc_idx", "score_hot",
            postgresql_using="btree",
            postgresql_ops={
                'score_hot':'DESC'
                }
            ),
        Index(
            "comments_score_top_desc_idx", "score_top",
            postgresql_using="btree",
            postgresql_ops={
                'score_top':'DESC'
                }
            ),
        Index(
            "comments_score_disputed_desc_idx", "score_disputed",
            postgresql_using="btree",
            postgresql_ops={
                'score_disputed':'DESC'
                }
            ),
        Index(
            "comments_created_utc_desc_idx", "created_utc",
            postgresql_using="btree",
            postgresql_ops={
                'created_utc':'DESC'
                }
            )
        )


    #flag_count=deferred(Column(Integer, server_default=FetchedValue()))

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

        if "created_utc" not in kwargs:
            kwargs["created_utc"] = g.timestamp

        kwargs["creation_ip"] = request.remote_addr

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

    def __repr__(self):

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

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

    @property
    @lazy
    def is_deleted(self):
        return bool(self.deleted_utc)

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

    @property
    def is_archived(self):
        return self.post.is_archived

    @property
    @lazy
    def parent(self):

        if not self.parent_submission:
            return None

        if self.is_top_level:
            return self.post

        else:
            return g.db.query(Comment).get(self.parent_comment_id)

    @property
    def children(self):

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

    @property
    def replies(self):

        r = self.__dict__.get("replies", None)
        if r is None:
            r = self.child_comments
        return r

    @replies.setter
    def replies(self, value):
        self.__dict__["replies"] = value

    @property
    @lazy
    def permalink(self):

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

    @property
    @lazy
    def votes_permalink(self):
        return self.permalink.replace('/post/', '/votes/', 1)

    @property
    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])

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

    def visibility_reason(self, v):
        if not v or self.author_id == v.id:
            return "this is your content."
        elif not self.board:
            return None
        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."

    def determine_offensive(self):

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

                break
        else:
            self.is_offensive = False

    @property
    def json_raw(self):
        data= {
            'id': self.base36id,
            'fullname': self.fullname,
            'level': self.level,
            'author_name': self.author.username if not self.author.is_deleted else None,
            'body': self.body,
            'body_html': self.body_html,
            'is_archived': self.is_archived,
            'is_bot': self.is_bot,
            'created_utc': self.created_utc,
            'edited_utc': self.edited_utc or 0,
            'is_banned': bool(self.is_banned),
            'is_deleted': self.is_deleted,
            'is_nsfw': self.over_18,
            'is_offensive': self.is_offensive,
            'is_nsfl': self.is_nsfl,
            'is_distinguished': bool(self.distinguish_level),
            'is_heralded': bool(self.gm_distinguish),
            'herald_guild': self.distinguished_board.name if self.gm_distinguish else None,
            'permalink': self.permalink,
            'post_id': self.post.base36id,
            'score': self.score_fuzzed,
            'upvotes': self.upvotes_fuzzed,
            'downvotes': self.downvotes_fuzzed,
            'award_count': self.award_count,
            'is_bot': self.is_bot,
            'guild_id': base36encode(self.post.board_id),
            'voted': self.voted
            }

        if self.ban_reason:
            data["ban_reason"]=self.ban_reason

        return data


    @property
    def json_core(self):
        if self.is_banned:
            data= {'is_banned': True,
                    'ban_reason': self.ban_reason,
                    'id': self.base36id,
                    'post': self.post.base36id,
                    'level': self.level,
                    'parent': self.parent_fullname
                    }
        elif self.deleted_utc > 0:
            data= {'deleted_utc': self.deleted_utc,
                    'id': self.base36id,
                    'post': self.post.base36id,
                    'level': self.level,
                    'parent': self.parent_fullname
                    }
        else:
            data=self.json_raw

            if self.level>=2:
                data['parent_comment_id']= base36encode(self.parent_comment_id),

        if "replies" in self.__dict__:
            data['replies']=[x.json_core for x in self.replies]

        return data

    @property
    def json(self):
    
        data=self.json_core

        if self.deleted_utc > 0 or self.is_banned:
            return data

        data["author"]=self.author.json_core
        data["post"]=self.post.json_core
        data["guild"]=self.post.board.json_core
        data["voted"]=self.voted

        if self.level >= 2:
            data["parent"]=self.parent.json_core if self.parent else []


        return data

        
    @property
    def voted(self):
        return self.__dict__.get("_voted")
        
    @property
    def title(self):
        return self.__dict__.get("_title", self.author.title)

    @property
    def is_blocking(self):
        return self.__dict__.get('_is_blocking', 0)

    @property
    def is_blocked(self):
        return self.__dict__.get('_is_blocked', 0)

    @property
    def body(self):
        return self.comment_aux.body

    @body.setter
    def body(self, x):
        self.comment_aux.body = x
        g.db.add(self.comment_aux)

    @property
    def body_html(self):
        return self.comment_aux.body_html

    @body_html.setter
    def body_html(self, x):
        self.comment_aux.body_html = x
        g.db.add(self.comment_aux)

    @property
    def ban_reason(self):
        return self.comment_aux.ban_reason

    @ban_reason.setter
    def ban_reason(self, x):
        self.comment_aux.ban_reason = x
        g.db.add(self.comment_aux)

    @property
    def flag_count(self):
        return len(self.flags)

    @property
    def award_count(self):
        return len(self.awards)

    def collapse_for_user(self, v):

        if not v:
            return False

        if self.is_offensive and v.hide_offensive:
            return True
			
        if self.is_bot and v.hide_bot:
            return True

        if any([x in self.body for x in v.filter_words]):
            return True

        return False

    @property
    def self_download_json(self):

        #This property should never be served to anyone but author and admin
        if not self.is_banned and not self.is_banned:
            return self.json_core

        data= {
            "author": self.author.name,
            "body": self.body,
            "body_html": self.body_html,
            "is_banned": bool(self.is_banned),
            "deleted_utc": self.deleted_utc,
            'created_utc': self.created_utc,
            'id': self.base36id,
            'fullname': self.fullname,
            'permalink': self.permalink,
            'post_id': self.post.base36id,
            'level': self.level
        }
        if self.level>=2:
            data['parent_comment_id']= base36encode(self.parent_comment_id)

        return data

    @property
    def json_admin(self):
        data= self.json_raw

        data["creation_ip"] = self.creation_ip
        data["creation_region"] = self.creation_region
    
        return data

    def is_guildmaster(self, perm=None):
        mod=self.__dict__.get('_is_guildmaster', False)

        if not mod:
            return False
        elif not perm:
            return True
        else:
            return mod.perm_full or mod.__dict__[f"perm_{perm}"]

        return output

    @property
    def is_exiled_for(self):
        return self.__dict__.get('_is_exiled_for', None)

    @property
    @lazy
    def is_op(self):
        return self.author_id==self.post.author_id and not self.author.is_deleted and not self.post.author.is_deleted and not self.post.is_deleted

    @property
    @lazy
    def board(self):
        return self.post.board

    @property
    def parent_fullname(self):
        if self.parent_comment_id:
            return f't3_{base36encode(self.parent_comment_id)}'
        elif self.parent_submission:
            return f't2_{base36encode(self.parent_submission)}'
        else:
            return None
        
    @property
    def notif_type(self):
        
        #system notif
        if self.author_id==1:
            return 1 
        
        #reply
        if g.v and self.parent.author_id==g.v.id:
            return 2
        
        #mention
        else:
            return 3
    
    @property
    def board_id(self):
        return self.post.board_id

    @property
    def outside_embed_url(self):
        return f"/embed/comment/{self.base36id}"
    

class Notification(Base):

    __tablename__ = "notifications"

    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey("users.id"), index=True)
    comment_id = Column(Integer, ForeignKey("comments.id"), index=True, default=None)
    submission_id = Column(Integer, ForeignKey("submissions.id"), default=None)

    read = Column(Boolean, default=False)

    comment = relationship("Comment")
    post = relationship("Submission")
    user=relationship("User", innerjoin=True, viewonly=True)

    def __repr__(self):

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

    @property
    def voted(self):
        return 0

    @property
    def target(self):
        return self.comment if self.comment_id else self.post

    @property
    def created_utc(self):
        return self.target.created_utc