import time from math import floor from random import randint from typing import Optional, TYPE_CHECKING from urllib.parse import parse_qs, urlencode, urlparse from flask import g from sqlalchemy import ForeignKey from sqlalchemy.dialects.postgresql import TSVECTOR from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.schema import FetchedValue from sqlalchemy.sql.sqltypes import * from files.classes import Base from files.helpers.config.const import * from files.helpers.config.awards import * from files.helpers.slurs_and_profanities import * from files.helpers.lazy import lazy from files.helpers.regex import * from files.helpers.sorting_and_time import * from files.helpers.types import comment_id_fk, int_pk, post_id_fk, user_id_fk from files.helpers.bleach_body import * from .saves import CommentSaveRelationship if TYPE_CHECKING: from files.classes import AwardRelationship, CasinoGame, CommentOption, CommentReport, OauthApp, Post, User def get_emoji_awards_emojis(obj, v, kind, NSFW_EMOJIS): if g.show_nsfw: emojis = [x.note for x in obj.awards if x.kind == kind] else: emojis = [x.note for x in obj.awards if x.kind == kind and x.note not in NSFW_EMOJIS] return reversed(emojis[:20]) def get_award_classes(obj, v, title=False): classes = [] if not (v and v.poor): if obj.award_count('glowie', v): classes.append("glow") if obj.award_count('gold', v): classes.append("gold-text") if obj.rainbowed: classes.append("rainbow-text") if obj.queened: classes.append("queen") if obj.sharpened: classes.append(f"sharpen") if not title: classes.append(f"chud-img sharpen-{obj.id_last_num}") if obj.chudded: classes.append("text-uppercase") if not title: classes.append(f"chud-img chud-{obj.id_last_num}") if IS_HOMOWEEN(): if obj.award_count('ectoplasm', v): classes.append("ectoplasm") if obj.award_count('candycorn', v): classes.append("candycorn") if obj.award_count('stab', v) and isinstance(obj, Comment): classes.append("blood") if IS_FISTMAS(): if obj.award_count('candycane', v): classes.append("candycane") return ' '.join(classes) def normalize_urls_runtime(body, v): if v and v.reddit != 'old.reddit.com': body = reddit_to_vreddit_regex.sub(rf'\1https://{v.reddit}/\2/', body) if v and v.nitter: body = twitter_to_nitter_regex.sub(r'\1https://nitter.unixfox.eu/', body) if v and v.imgsed: body = instagram_to_imgsed_regex.sub(r'\1https://imgsed.com/', body) if not v or v.controversial: captured = [] for i in controversial_regex.finditer(body): if i.group(0) in captured: continue captured.append(i.group(0)) url = i.group(0) p = urlparse(url).query p = parse_qs(p, keep_blank_values=True) if 'sort' not in p: p['sort'] = ['controversial'] url_noquery = url.split('?')[0] body = body.replace(f'{url}', f'{url_noquery}?{urlencode(p, True)}') return body def add_options(self, body, v): if 'details>' in body or 'summary>' in body: return body if isinstance(self, Comment): kind = 'comment' else: kind = 'post' if self.options: curr = [x for x in self.options if x.exclusive and x.voted(v)] if curr: curr = f" value=option-{kind}-" + str(curr[0].id) else: curr = '' body += f'' winner = [x for x in self.options if x.exclusive == 3] for o in self.options: option_body = '' if o.exclusive > 1: option_body += f'''
= POLL_BET_COINS) or self.total_bet_voted(v): option_body += " disabled " option_body += f'''>" if o.exclusive == 3: option_body += " - WINNER!" if not winner and v and v.admin_level >= PERMS['POST_BETS_DISTRIBUTE']: option_body += f'''''' option_body += "
" else: input_type = 'radio' if o.exclusive else 'checkbox' option_body += f'
''' if o.exclusive > 1: s = '##' elif o.exclusive: s = '&&' else: s = '$$' if f'{s}{o.body_html}{s}' in body: body = body.replace(f'{s}{o.body_html}{s}', option_body, 1) elif not o.created_utc or o.created_utc < 1677622270: body += option_body return body class Comment(Base): __tablename__ = "comments" id: Mapped[int_pk] author_id: Mapped[user_id_fk] parent_post: Mapped[Optional[post_id_fk]] wall_user_id: Mapped[Optional[user_id_fk]] created_utc: Mapped[int] edited_utc: Mapped[int] = mapped_column(default=0) is_banned: Mapped[bool] = mapped_column(default=False) ghost: Mapped[bool] = mapped_column(default=False) bannedfor: Mapped[Optional[str]] chuddedfor: Mapped[Optional[str]] distinguished: Mapped[bool] = mapped_column(default=False) deleted_utc: Mapped[int] = mapped_column(default=0) is_approved: Mapped[Optional[user_id_fk]] level: Mapped[int] = mapped_column(default=1) parent_comment_id: Mapped[Optional[comment_id_fk]] top_comment_id: Mapped[Optional[int]] is_bot: Mapped[bool] = mapped_column(default=False) stickied: Mapped[Optional[str]] stickied_utc: Mapped[Optional[int]] num_of_pinned_children: Mapped[int] = mapped_column(default=0) sentto: Mapped[Optional[user_id_fk]] app_id: Mapped[Optional[int]] = mapped_column(ForeignKey("oauth_apps.id")) upvotes: Mapped[int] = mapped_column(default=1) downvotes: Mapped[int] = mapped_column(default=0) realupvotes: Mapped[int] = mapped_column(default=1) body: Mapped[Optional[str]] body_html: Mapped[Optional[str]] body_ts: Mapped[str] = mapped_column(TSVECTOR(), server_default=FetchedValue()) ban_reason: Mapped[Optional[str]] treasure_amount: Mapped[Optional[str]] slots_result: Mapped[Optional[str]] ping_cost: Mapped[int] = mapped_column(default=0) blackjack_result: Mapped[Optional[str]] casino_game_id: Mapped[Optional[int]] = mapped_column(ForeignKey("casino_games.id")) chudded: Mapped[bool] = mapped_column(default=False) rainbowed: Mapped[bool] = mapped_column(default=False) queened: Mapped[bool] = mapped_column(default=False) sharpened: Mapped[bool] = mapped_column(default=False) if FEATURES['NSFW_MARKING']: nsfw: Mapped[bool] = mapped_column(default=False) else: nsfw = False oauth_app: Mapped["OauthApp"] = relationship() post: Mapped["Post"] = relationship(back_populates="comments") author: Mapped["User"] = relationship(primaryjoin="User.id==Comment.author_id") senttouser: Mapped["User"] = relationship(primaryjoin="User.id==Comment.sentto") parent_comment: Mapped["Comment"] = relationship(remote_side="Comment.id") awards: Mapped[list["AwardRelationship"]] = relationship(order_by="AwardRelationship.awarded_utc.desc()", back_populates="comment") reports: Mapped[list["CommentReport"]] = relationship(order_by="CommentReport.created_utc") options: Mapped[list["CommentOption"]] = relationship(order_by="CommentOption.id") casino_game: Mapped["CasinoGame"] = relationship() wall_user: Mapped["User"] = relationship(primaryjoin="User.id==Comment.wall_user_id") def __init__(self, *args, **kwargs): if "created_utc" not in kwargs: kwargs["created_utc"] = int(time.time()) super().__init__(*args, **kwargs) def __repr__(self): return f"<{self.__class__.__name__}(id={self.id})>" @property @lazy def top_comment(self): return g.db.get(Comment, self.top_comment_id) @property @lazy def controversial(self): if self.downvotes > 5 and 0.25 < self.upvotes / self.downvotes < 4: return True return False @property @lazy def age_string(self): notif_utc = self.__dict__.get("notif_utc") if notif_utc: timestamp = notif_utc elif self.created_utc: timestamp = self.created_utc else: return None return make_age_string(timestamp) @property @lazy def edited_string(self): if not self.edited_utc: return None return make_age_string(self.edited_utc) @property @lazy def score(self): return self.upvotes - self.downvotes @property @lazy def fullname(self): return f"c_{self.id}" @property @lazy def id_last_num(self): return str(self.id)[-1] @property @lazy def parent_fullname(self): if self.parent_comment_id: return f"c_{self.parent_comment_id}" elif self.parent_post: return f"p_{self.parent_post}" @lazy def replies(self, sort): if self.replies2 != None: return self.replies2 replies = g.db.query(Comment).filter_by(parent_comment_id=self.id).order_by(Comment.stickied, Comment.num_of_pinned_children.desc()) if not self.parent_post: sort='old' return sort_objects(sort, replies, Comment).all() @property def replies2(self): return self.__dict__.get("replies2") @replies2.setter def replies2(self, value): self.__dict__["replies2"] = value @property @lazy def shortlink(self): if self.wall_user_id: return f"/@{self.wall_user.username}/wall/comment/{self.id}#context" if self.parent_post: return f"{self.post.shortlink}/{self.id}#context" return f"/notification/{self.id}" @property @lazy def permalink(self): return f"{SITE_FULL}{self.shortlink}" @property @lazy def log_link(self): return f"{SITE_FULL}/transfers/{self.id}" @property @lazy def more_comments(self): if self.wall_user_id: return f"/@{self.wall_user.username}/wall/comment/{self.id}?context=0#context" return f"{self.post.permalink}/{self.id}?context=0#context" @property @lazy def author_name(self): if self.ghost and not (hasattr(g, 'v') and g.v and self.id == g.v.id): return '👻' return self.author.user_name @property @lazy def author_name_punish_modal(self): if self.ghost and not (hasattr(g, 'v') and g.v and self.id == g.v.id): return '👻' return self.author.username @lazy def award_count(self, kind, v): if v and v.poor: return 0 return len([x for x in self.awards if x.kind == kind]) @property @lazy def json(self): if self.is_banned: data = {'is_banned': True, 'ban_reason': self.ban_reason, 'id': self.id, 'post': self.post.id if self.post else 0, 'level': self.level, 'parent': self.parent_fullname } elif self.deleted_utc: data = {'deleted_utc': self.deleted_utc, 'id': self.id, 'post': self.post.id if self.post else 0, 'level': self.level, 'parent': self.parent_fullname } else: reports = {} for r in self.reports: reports[r.user.username] = r.reason data = { 'id': self.id, 'level': self.level, 'author_id': '👻' if self.ghost else self.author_id, 'author_name': self.author_name, 'body': self.body, 'body_html': self.body_html, 'is_bot': self.is_bot, 'created_utc': self.created_utc, 'edited_utc': self.edited_utc or 0, 'is_banned': bool(self.is_banned), 'deleted_utc': self.deleted_utc, 'is_nsfw': self.nsfw, 'permalink': f'/comment/{self.id}#context', 'stickied': self.stickied, 'distinguished': self.distinguished, 'post_id': self.post.id if self.post else 0, 'score': self.score, 'upvotes': self.upvotes, 'downvotes': self.downvotes, 'is_bot': self.is_bot, 'reports': reports, 'replies': [x.id for x in self.replies(sort="old")] } if self.level >= 2: data['parent_comment_id'] = self.parent_comment_id return data @lazy def total_bet_voted(self, v): if "closed" in self.body.lower(): return True if v: for o in self.options: if o.exclusive == 3: return True if o.exclusive == 2 and o.voted(v): return True return False @lazy def total_poll_voted(self, v): if v: if v.id == self.author_id: return True for o in self.options: if o.voted(v): return True return False @lazy def realbody(self, v): if self.deleted_utc != 0 and not (v and (v.admin_level >= PERMS['POST_COMMENT_MODERATION'] or v.id == self.author.id)): return "[Deleted by user]" if self.is_banned and not (v and v.admin_level >= PERMS['POST_COMMENT_MODERATION']) and not (v and v.id == self.author.id): return "" body = self.body_html or "" body = add_options(self, body, v) if body: if not (self.parent_post and self.post.hole == 'chudrama'): body = censor_slurs_profanities(body, v) body = normalize_urls_runtime(body, v) body = bleach_body_html(body, runtime=True) #to stop slur filters and poll options being used as a vector for html/js injection return body @lazy def plainbody(self, v): if self.deleted_utc != 0 and not (v and (v.admin_level >= PERMS['POST_COMMENT_MODERATION'] or v.id == self.author.id)): return "[Deleted by user]" if self.is_banned and not (v and v.admin_level >= PERMS['POST_COMMENT_MODERATION']) and not (v and v.id == self.author.id): return "" body = self.body if not body: return "" if not (self.parent_post and self.post.hole == 'chudrama'): body = censor_slurs_profanities(body, v, True) return body @lazy def collapse_for_user(self, v, comment_info, path=''): if v and self.author_id == v.id: return False if path == '/admin/removed/comments': return False if comment_info: return False if self.nsfw and not (any(path.startswith(x) for x in ('/post/','/comment/','/h/')) and self.post.nsfw) and not g.show_nsfw: return True if self.is_banned: return True if self.author.shadowbanned: return True if v and v.filter_words and self.body and any(x in self.body for x in v.filter_words): return True return False @property @lazy def is_op(self): return self.author_id==self.post.author_id @lazy def filtered_reports(self, v): return [r for r in self.reports if not r.user.shadowbanned or (v and v.id == r.user_id) or (v and v.admin_level)] @lazy def active_reports(self, v): return len(self.filtered_reports(v)) @property @lazy def blackjack_html(self): if not self.blackjack_result: return '' split_result = self.blackjack_result.split('_') blackjack_status = split_result[3] player_hand = split_result[0].replace('X', '10') dealer_hand = split_result[1].split('/')[0] if blackjack_status == 'active' else split_result[1] dealer_hand = dealer_hand.replace('X', '10') wager = int(split_result[4]) try: kind = split_result[5] except: kind = "coins" currency_kind = "Coins" if kind == "coins" else "Marseybux" try: is_insured = split_result[6] except: is_insured = "0" body = f"{player_hand} vs. {dealer_hand}" if blackjack_status == 'push': body += f"Pushed. Refunded {wager} {currency_kind}." elif blackjack_status == 'bust': body += f"Bust. Lost {wager} {currency_kind}." elif blackjack_status == 'lost': body += f"Lost {wager} {currency_kind}." elif blackjack_status == 'won': body += f"Won {wager} {currency_kind}." elif blackjack_status == 'blackjack': body += f"Blackjack! Won {floor(wager * 3/2)} {currency_kind}." if is_insured == "1": body += " Insured." body += '' return body @property @lazy def num_savers(self): return g.db.query(CommentSaveRelationship).filter_by(comment_id=self.id).count() @lazy def award_classes(self, v): return get_award_classes(self, v) @lazy def emoji_awards_emojis(self, v, kind, NSFW_EMOJIS): return get_emoji_awards_emojis(self, v, kind, NSFW_EMOJIS) @property @lazy def is_longpost(self): return len(self.body) >= 2000 def pin_parents(self): c = self while c.level > 2: c = c.parent_comment c.num_of_pinned_children += 1 g.db.add(c) def unpin_parents(self): c = self while c.level > 2: c = c.parent_comment c.num_of_pinned_children -= 1 if c.num_of_pinned_children < 0: c.num_of_pinned_children = 0 g.db.add(c)