Update pollchat.py
This commit is contained in:
103
pollchat.py
103
pollchat.py
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user