Update pollchat.py

This commit is contained in:
2026-03-27 04:31:38 +00:00
parent fbe3092410
commit a461b0d8ea

View File

@@ -48,27 +48,33 @@ def bg(fg, bgc):
LETTERS = list(string.ascii_lowercase)
import re
import unicodedata
_ALLOWED = re.compile(
r'^['
r'a-zA-Z0-9'
r' '
r'!@#$%^&*()_+\-=\[\]{};\':"\\|,.<>/?`~'
r'\U0001F000-\U0001FFFF'
r'\U00002600-\U000027BF'
r'\U0000FE00-\U0000FE0F'
r'\U0000200D'
r']+$'
_STRIP_IRC = re.compile(r'\x03(\d{1,2}(,\d{1,2})?)?|[\x02\x0f\x1d\x1f\x16]')
_EMOJI_RANGES = (
(0x2600, 0x27BF),
(0x2B50, 0x2B55),
(0xFE00, 0xFE0F),
(0x200D, 0x200D),
(0x1F000, 0x1FFFF),
(0xE0020, 0xE007F),
)
def _is_emoji(c):
o = ord(c)
return any(lo <= o <= hi for lo, hi in _EMOJI_RANGES)
def sanitize(text: str) -> str | None:
cleaned = text.strip()
if not cleaned:
stripped = _STRIP_IRC.sub('', text).strip()
if not stripped:
return None
if not _ALLOWED.match(cleaned):
for c in stripped:
if c.isascii() and c.isprintable():
continue
if _is_emoji(c):
continue
return None
return cleaned
return text.strip()
class Poll:
@@ -110,6 +116,9 @@ class Poll:
if self.custom_count >= MAX_CUSTOM_OPTIONS:
return f'{RED}Max custom options reached ({MAX_CUSTOM_OPTIONS}). Vote for an existing option.{RESET}'
if nick in self.custom_by.values():
return f'{RED}You already added a custom option.{RESET}'
response = sanitize(response)
if not response:
return f'{RED}Invalid characters in response.{RESET}'
@@ -125,6 +134,7 @@ class Poll:
if nick in self.votes:
old = self.votes[nick]
self._cleanup_empty_custom(old)
self.votes[nick] = new_letter
return f'{GREEN}Added {BOLD}{new_letter}. {response}{BOLD} and changed vote from {BOLD}{old}{RESET}'
self.votes[nick] = new_letter
@@ -135,11 +145,37 @@ class Poll:
if old == choice:
return f'{YELLOW}You already voted for {BOLD}{choice}{RESET}'
self.votes[nick] = choice
self._cleanup_empty_custom(old)
return f'{GREEN}Changed vote from {BOLD}{old}{BOLD} to {BOLD}{choice}{RESET}'
self.votes[nick] = choice
return f'{GREEN}Vote recorded for {BOLD}{choice}{RESET}'
def _cleanup_empty_custom(self, letter: str):
idx = ord(letter) - ord('a')
if idx not in self.custom_by:
return
if any(v == letter for v in self.votes.values()):
return
creator = self.custom_by.pop(idx)
self.options.pop(idx)
self.custom_count -= 1
new_custom_by = {}
for old_idx, cn in self.custom_by.items():
new_custom_by[old_idx - 1 if old_idx > idx else old_idx] = cn
self.custom_by = new_custom_by
new_votes = {}
for vn, vl in self.votes.items():
vi = ord(vl) - ord('a')
if vi > idx:
new_votes[vn] = LETTERS[vi - 1]
else:
new_votes[vn] = vl
self.votes = new_votes
def results_lines(self) -> list[str]:
total = len(self.votes)
lines = []
@@ -178,6 +214,7 @@ class Bot:
self.writer = None
self.current_poll = None
self.last_poll_at = 0
self.last_cmd = {} # nick -> timestamp
async def connect(self):
if SSL:
@@ -241,6 +278,11 @@ class Bot:
await self.on_message(nick, msg)
async def on_message(self, nick: str, msg: str):
now = time.time()
if now - self.last_cmd.get(nick, 0) < 3:
return
self.last_cmd[nick] = now
if msg.startswith('.poll'):
args = msg[5:].strip()
@@ -266,11 +308,21 @@ class Bot:
self.privmsg(f'{RED}Invalid characters in question.{RESET}')
return
if len(_STRIP_IRC.sub('', question)) > 100:
self.privmsg(f'{RED}Question must be 100 characters or less.{RESET}')
return
options = []
for o in raw_options.split(','):
cleaned = sanitize(o.strip())
if cleaned:
options.append(cleaned)
if not cleaned:
continue
if cleaned.lower() == 'other':
continue
if len(_STRIP_IRC.sub('', cleaned)) > 20:
self.privmsg(f'{RED}Each option must be 20 characters or less.{RESET}')
return
options.append(cleaned)
if len(options) < 2:
self.privmsg(f'{RED}Need at least {BOLD}2{BOLD} options.{RESET}')
@@ -280,18 +332,16 @@ class Bot:
self.privmsg(f'{RED}Maximum {BOLD}10{BOLD} options.{RESET}')
return
options.append('other')
self.current_poll = Poll(question, options, nick)
self.last_poll_at = now
for line in self.current_poll.results_lines():
self.privmsg(line)
vote_hint = f'{GREY}Vote with {BOLD}.vote <letter>{BOLD}'
has_other = any(o.lower() == 'other' for o in options)
if has_other:
vote_hint += f' • For "other": {BOLD}.vote {LETTERS[next(i for i, o in enumerate(options) if o.lower() == "other")]} your response{BOLD}'
vote_hint += RESET
self.privmsg(vote_hint)
other_letter = LETTERS[len(options) - 1]
self.privmsg(f'{GREY}Vote with {BOLD}.vote <letter>{BOLD} • For "other": {BOLD}.vote {other_letter} your response{RESET}')
return
if msg.startswith('.vote'):
@@ -316,10 +366,6 @@ class Bot:
self.privmsg(result)
return
if msg.startswith('.results'):
await self.show_poll()
return
if msg.strip() == '@polls':
self.show_help()
return
@@ -340,15 +386,12 @@ class Bot:
('.poll <q> | <a>, <b>, ...', 'Create a new poll'),
('.vote <letter>', 'Vote for an option'),
('.vote <letter> <text>', 'Vote "other" with custom response'),
('.results', 'Show current poll results'),
('@polls', 'Show this help menu'),
]
for cmd, desc in cmds:
h.append(f' {BOLD}{YELLOW}{cmd:<28}{RESET}{WHITE}{desc}{RESET}')
h.append(f' {GREY}Cooldown: 10min between polls • Max 10 custom "other" options{RESET}')
for line in h:
self.privmsg(line)