Already using TalkSpar? Sign in
The gist
On TalkSpar, you're free to share and say anything protected by the First Amendment. No censorship. No shadowbanning. No bullshit.
Share articles, photos, or videos found on the web in topic-specific communities called guilds.
Comment on other people's posts and engage in boundless, free discussion without censorship.
Vote on content you like or dislike. The content with the most upvotes makes it to the top!
Open source
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.
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
get started
Create a TalkSpar account in seconds and join your favorite communities. Usernames aplenty. No email required.
Sign up