diff --git a/files/classes/comment.py b/files/classes/comment.py index c9f9e330f..4dc99bdc9 100644 --- a/files/classes/comment.py +++ b/files/classes/comment.py @@ -276,20 +276,29 @@ class Comment(Base): for o in self.options: input_type = 'radio' if o.exclusive else 'checkbox' - body += f'
''' + option_body += f'''>''' + + if o.exclusive > 1: s = '!!' + elif o.exclusive: s = '&&' + else: s = '$$' + + if f'{s}{o.body}{s}' in body: + body = body.replace(f'{s}{o.body}{s}', option_body) + else: + body += option_body if not self.ghost and self.author.show_sig(v): body += f'

{self.author.sig_html}
' diff --git a/files/classes/polls.py b/files/classes/polls.py index c640fde41..3578b4219 100644 --- a/files/classes/polls.py +++ b/files/classes/polls.py @@ -11,7 +11,8 @@ class SubmissionOption(Base): __tablename__ = "submission_options" id = Column(Integer, primary_key=True) - submission_id = Column(Integer, ForeignKey("submissions.id")) + parent_id = Column(Integer, ForeignKey("submissions.id")) + body = Column(Text) body_html = Column(Text) exclusive = Column(Integer) created_utc = Column(Integer) @@ -66,7 +67,8 @@ class CommentOption(Base): __tablename__ = "comment_options" id = Column(Integer, primary_key=True) - comment_id = Column(Integer, ForeignKey("comments.id")) + parent_id = Column(Integer, ForeignKey("comments.id")) + body = Column(Text) body_html = Column(Text) exclusive = Column(Integer) created_utc = Column(Integer) diff --git a/files/classes/submission.py b/files/classes/submission.py index c4f1f00ab..489818a1f 100644 --- a/files/classes/submission.py +++ b/files/classes/submission.py @@ -285,39 +285,49 @@ class Submission(Base): winner = [x for x in self.options if x.exclusive == 3] for o in self.options: - if o.exclusive > 1: - body += f'''
= POLL_BET_COINS) or self.total_bet_voted(v): body += " disabled " + option_body = '' - body += f'''>
" else: input_type = 'radio' if o.exclusive else 'checkbox' - body += f'
''' + option_body += f'''>''' + if o.exclusive > 1: s = '!!' + elif o.exclusive: s = '&&' + else: s = '$$' + + if f'{s}{o.body}{s}' in body: + body = body.replace(f'{s}{o.body}{s}', option_body) + else: + body += option_body if not listing and not self.ghost and self.author.show_sig(v): body += f'

{self.author.sig_html}
' diff --git a/files/helpers/actions.py b/files/helpers/actions.py index d3fd81962..2822e84e1 100644 --- a/files/helpers/actions.py +++ b/files/helpers/actions.py @@ -466,28 +466,52 @@ def execute_lawlz_actions(v:User, p:Submission): g.db.add(ma_2) g.db.add(ma_3) -def process_poll_options(target:Union[Submission, Comment], - cls:Union[Type[SubmissionOption], Type[CommentOption]], - options:Iterable[str], exclusive:int, friendly_name:str, - db:scoped_session) -> None: - for option in options: - if len(option) > 500: abort(400, f"{friendly_name} option too long!") - if cls is SubmissionOption: - option = cls( - submission_id=target.id, - body_html=option, - exclusive=exclusive, - ) - else: - option = cls( - comment_id=target.id, - body_html=option, - exclusive=exclusive, - ) - db.add(option) def execute_wordle(c:Comment, body:str): if not FEATURES['WORDLE']: return if not "!wordle" in body: return answer = random.choice(WORDLE_LIST) c.wordle_result = f'_active_{answer}' + + +def process_poll_options(v:User, target:Union[Submission, Comment]): + + patterns = [(poll_regex, 0), (choice_regex, 1)] + + if isinstance(target, Submission) and v and v.admin_level >= PERMS['POST_BETS']: + patterns.append((bet_regex, 2)) + + option_count = 0 + + for pattern, exclusive in patterns: + for i in pattern.finditer(target.body): + option_count += 1 + + if option_count > POLL_MAX_OPTIONS: + abort(400, f"Max number of poll options is {POLL_MAX_OPTIONS}") + + body = i.group(1) + + if len(body) > 500: + abort(400, f"Poll option body too long! (Max 500 characters)") + + if isinstance(target, Submission): + cls = SubmissionOption + else: + cls = CommentOption + + g.db.flush() + existing = g.db.query(cls).filter_by( + parent_id=target.id, + body=body, + exclusive=exclusive, + ).one_or_none() + + if not existing: + option = cls( + parent_id=target.id, + body=body, + body_html=filter_emojis_only(body), + exclusive=exclusive, + ) + g.db.add(option) diff --git a/files/helpers/regex.py b/files/helpers/regex.py index a4f9fd964..9595264f4 100644 --- a/files/helpers/regex.py +++ b/files/helpers/regex.py @@ -30,7 +30,7 @@ valid_sub_regex = re.compile("^[a-zA-Z0-9_\-]{3,25}$", flags=re.A) query_regex = re.compile("(\w+):(\S+)", flags=re.A) poll_regex = re.compile("\s*\$\$([^\$\n]+)\$\$\s*(?!([^<]*<\/(code|pre|a)>|[^`]*`))", flags=re.A) -bet_regex = re.compile("\s*\$\$\$([^\$\n]+)\$\$\$\s*(?!([^<]*<\/(code|pre|a)>|[^`]*`))", flags=re.A) +bet_regex = re.compile("\s*!!([^\$\n]+)!!\s*(?!([^<]*<\/(code|pre|a)>|[^`]*`))", flags=re.A) choice_regex = re.compile("\s*&&([^\$\n]+)&&\s*(?!([^<]*<\/(code|pre|a)>|[^`]*`))", flags=re.A) html_comment_regex = re.compile("", flags=re.A) diff --git a/files/helpers/sanitize.py b/files/helpers/sanitize.py index 5244eb581..504af00eb 100644 --- a/files/helpers/sanitize.py +++ b/files/helpers/sanitize.py @@ -647,18 +647,3 @@ def validate_css(css): return False, f"The domain '{domain}' is not allowed, please use one of these domains\n\n{approved_embed_hosts}." return True, "" - -def sanitize_poll_options(v:User, body:str, allow_bets:bool) -> tuple[str, List[Any], List[Any], List[Any]]: - def sanitize_poll_type(body:str, re:re.Pattern) -> tuple[str, List[str]]: - opts = [] - for i in list(re.finditer(body))[:POLL_MAX_OPTIONS] if POLL_MAX_OPTIONS else list(re.finditer(body)): - opts.append(filter_emojis_only(i.group(1))) - body = body.replace(i.group(0), "") - return (body, opts) - - bets = [] - if allow_bets and v and v.admin_level >= PERMS['POST_BETS']: - body, bets = sanitize_poll_type(body, bet_regex) - body, options = sanitize_poll_type(body, poll_regex) - body, choices = sanitize_poll_type(body, choice_regex) - return (body, bets, options, choices) diff --git a/files/routes/comments.py b/files/routes/comments.py index cdb8eb1af..d61ed9def 100644 --- a/files/routes/comments.py +++ b/files/routes/comments.py @@ -150,8 +150,6 @@ def comment(v:User): if not v.admin_level >= PERMS['POST_COMMENT_MODERATION'] and parent_user.any_block_exists(v): abort(403, "You can't reply to users who have blocked you or users that you have blocked!") - body, _, options, choices = sanitize_poll_options(v, body, False) - if request.files.get("file") and not g.is_tor: files = request.files.getlist('file')[:20] @@ -264,15 +262,14 @@ def comment(v:User): g.db.add(c) g.db.flush() + process_poll_options(v, c) + execute_blackjack(v, c, c.body, "comment") execute_under_siege(v, c, c.body, "comment") if c.level == 1: c.top_comment_id = c.id else: c.top_comment_id = parent.top_comment_id - process_poll_options(c, CommentOption, options, 0, "Poll", g.db) - process_poll_options(c, CommentOption, choices, 1, "Poll", g.db) - if post_target.id not in ADMIGGER_THREADS and v.agendaposter and not v.marseyawarded and AGENDAPOSTER_PHRASE not in c.body.lower() and not (posting_to_submission and post_target.sub == 'chudrama'): c.is_banned = True c.ban_reason = "AutoJanny" @@ -403,10 +400,6 @@ def edit_comment(cid, v): elif v.bird and len(body) > 140: abort(403, "You have to type less than 140 characters!") - body, _, options, choices = sanitize_poll_options(v, body, False) - process_poll_options(c, CommentOption, options, 0, "Poll", g.db) - process_poll_options(c, CommentOption, choices, 1, "Poll", g.db) - execute_antispam_comment_check(body, v) body = process_files(request.files, v, body) @@ -428,6 +421,9 @@ def edit_comment(cid, v): abort(403, "You can only type marseys!") c.body = body + + process_poll_options(v, c) + c.body_html = body_html execute_blackjack(v, c, c.body, "comment") diff --git a/files/routes/polls.py b/files/routes/polls.py index f0fcf31d6..2faaae84e 100644 --- a/files/routes/polls.py +++ b/files/routes/polls.py @@ -35,7 +35,7 @@ def vote_option(option_id, v): if option.exclusive: vote = g.db.query(SubmissionOptionVote).join(SubmissionOption).filter( SubmissionOptionVote.user_id==v.id, - SubmissionOptionVote.submission_id==option.submission_id, + SubmissionOptionVote.submission_id==option.parent_id, SubmissionOption.exclusive==option.exclusive).all() if vote: if option.exclusive == 2: abort(400, "You already voted on this bet!") @@ -47,7 +47,7 @@ def vote_option(option_id, v): vote = SubmissionOptionVote( option_id=option_id, user_id=v.id, - submission_id=option.submission_id, + submission_id=option.parent_id, ) g.db.add(vote) elif existing and not option.exclusive: @@ -107,7 +107,7 @@ def vote_option_comment(option_id, v): if option.exclusive: vote = g.db.query(CommentOptionVote).join(CommentOption).filter( CommentOptionVote.user_id==v.id, - CommentOptionVote.comment_id==option.comment_id, + CommentOptionVote.comment_id==option.parent_id, CommentOption.exclusive==1).one_or_none() if vote: g.db.delete(vote) @@ -117,7 +117,7 @@ def vote_option_comment(option_id, v): vote = CommentOptionVote( option_id=option_id, user_id=v.id, - comment_id=option.comment_id, + comment_id=option.parent_id, ) g.db.add(vote) elif existing: diff --git a/files/routes/posts.py b/files/routes/posts.py index f905b0eeb..a62d7018e 100644 --- a/files/routes/posts.py +++ b/files/routes/posts.py @@ -294,11 +294,6 @@ def edit_post(pid, v): body = body.strip()[:POST_BODY_LENGTH_LIMIT(v)] # process_files() may be adding stuff to the body if body != p.body: - body, bets, options, choices = sanitize_poll_options(v, body, False) - process_poll_options(p, SubmissionOption, bets, 2, "Bet", g.db) - process_poll_options(p, SubmissionOption, options, 0, "Poll", g.db) - process_poll_options(p, SubmissionOption, choices, 1, "Poll", g.db) - torture = (v.agendaposter and not v.marseyawarded and p.sub != 'chudrama' and v.id == p.author_id) body_html = sanitize(body, golden=False, limit_pings=100, showmore=False, torture=torture) @@ -309,6 +304,8 @@ def edit_post(pid, v): p.body = body + process_poll_options(v, p) + execute_under_siege(v, p, p.body, 'submission') for text in [p.body, p.title, p.url]: @@ -649,8 +646,6 @@ def submit_post(v:User, sub=None): if len(url) > 2048: abort(400, "There's a 2048 character limit for URLs!") - body, bets, options, choices = sanitize_poll_options(v, body, True) - body = process_files(request.files, v, body) body = body.strip()[:POST_BODY_LENGTH_LIMIT(v)] # process_files() adds content to the body, so we need to re-strip @@ -699,13 +694,11 @@ def submit_post(v:User, sub=None): g.db.add(post) g.db.flush() + process_poll_options(v, post) + for text in {post.body, post.title, post.url}: if execute_blackjack(v, post, text, 'submission'): break - process_poll_options(post, SubmissionOption, bets, 2, "Bet", g.db) - process_poll_options(post, SubmissionOption, options, 0, "Poll", g.db) - process_poll_options(post, SubmissionOption, choices, 1, "Poll", g.db) - vote = Vote(user_id=v.id, vote_type=1, submission_id=post.id diff --git a/migrations/20230228-position-options.sql b/migrations/20230228-position-options.sql new file mode 100644 index 000000000..ed2c6031c --- /dev/null +++ b/migrations/20230228-position-options.sql @@ -0,0 +1,11 @@ +alter table submission_options add column body character varying(500); +update submission_options set body=body_html; +alter table submission_options alter column body set not null; + +alter table comment_options add column body character varying(500); +update comment_options set body=body_html; +alter table comment_options alter column body set not null; + +alter table submission_options rename column submission_id to parent_id; + +alter table comment_options rename column comment_id to parent_id; diff --git a/schema.sql b/schema.sql index f3ab57966..90e07d51f 100644 --- a/schema.sql +++ b/schema.sql @@ -327,7 +327,8 @@ CREATE TABLE public.comment_option_votes ( CREATE TABLE public.comment_options ( id integer DEFAULT nextval('public.comment_option_id_seq'::regclass) NOT NULL, - comment_id integer NOT NULL, + parent_id integer NOT NULL, + body character varying(500) NOT NULL, body_html character varying(500) NOT NULL, exclusive integer NOT NULL, created_utc integer @@ -870,7 +871,8 @@ CREATE TABLE public.submission_option_votes ( CREATE TABLE public.submission_options ( id integer DEFAULT nextval('public.submission_option_id_seq'::regclass) NOT NULL, - submission_id integer NOT NULL, + parent_id integer NOT NULL, + body character varying(500) NOT NULL, body_html character varying(500) NOT NULL, exclusive integer NOT NULL, created_utc integer @@ -2872,4 +2874,3 @@ ALTER TABLE ONLY public.comments -- -- PostgreSQL database dump complete -- -