class Features(object): DONGER_SONG_TEMPLATE = ( 'I like to raise my {donger} I do it all the time ヽ༼ຈل͜ຈ༽ノ ' 'and every time its lowered┌༼ຈل͜ຈ༽┐ ' 'I cry and start to whine ┌༼@ل͜@༽┐' 'But never need to worry ༼ ºل͟º༽ ' 'my {donger}\'s staying strong ヽ༼ຈل͜ຈ༽ノ' 'A {donger} saved is a {donger} earned so sing the {donger} song! ' 'ᕦ༼ຈل͜ຈ༽ᕤ') CHATOT_TEMPLATE = ('░░░░░░░░░░░░░░░░░█░░░░ ' '░░░░░░░░░░░░░░░░███▄░░ ' '░░░░░░░░░▄██▄░▄██████░ ' '░░░░░░▄█████████░░████ ' '░░░░▒▒▒███▀█████░░░▀██ ' '░░▒▒▒▒▒▒█▄▀▄████░░░░█░ ' '░░▒░░▒▒██████▀░░░░░░░░ ' '░░░░░░░░▀▀▀▀░░░░░░░░░ ') TOO_LONG_TEXT_TEMPLATE = '{} Message length exceeds my capabilities!' MAIL_MAX_LEN = 500 def __init__(self, bot: Bot, help_text: str, database: Database, config: dict): self._bot = bot self._help_text = help_text self._database = database self._config = config self._recent_messages_for_regex = collections.defaultdict( lambda: collections.deque(maxlen=100)) self._last_message = {} self._spam_limiter = Limiter(min_interval=10) self._password_api_limiter = Limiter(min_interval=2) self._user_list = collections.defaultdict(set) self._regex_server = RegexServer() self._token_notifier = TokenNotifier( config.get('token_notify_filename'), config.get('token_notify_channels'), config.get('token_notify_interval', 60)) self._tellnext_generator = None if os.path.isfile(config.get('tellnext_database', '')): self._tellnext_generator = TellnextGenerator( config['tellnext_database']) self._match_generator = None if os.path.isfile(config.get('veekun_pokedex_database', '')): self._match_generator = MatchGenerator( config['veekun_pokedex_database']) self._battlebot = BattleBot(config['veekun_pokedex_database'], self._bot) bot.register_message_handler('pubmsg', self._battlebot.message_callback) bot.register_message_handler('whisper', self._battlebot.message_callback) self._mail_disabled_channels = config.get('mail_disabled_channels') self._avoid_pikalaxbot = config.get('avoid_pikalaxbot') bot.register_message_handler('pubmsg', self._collect_recent_message) bot.register_message_handler('action', self._collect_recent_message) bot.register_command(r'!?s/(.+/.*)', self._regex_command) bot.register_command(r'(?i)!caw($|\s.*)', self._caw_command) bot.register_command(r'(?i)!countdown($|\s.*)', self._countdown_command) bot.register_command(r'(?i)!debugecho\s+(.*)', self._debug_echo_command) bot.register_command(r'(?i)!double(team)?($|\s.*)', self._double_command) bot.register_command(r'(?i)!(set)?greet(ing)?($|\s.*)$', self._greeting_command) bot.register_command(r'(?i)!(groudonger)?(help|commands)($|\s.*)', self._help_command) bot.register_command(r'(?i)!groudon(ger)?($|\s.*)', self._roar_command) bot.register_command(r'(?i)!huffle($|\s.*)', self._huffle_command) bot.register_command(r'(?i)!hypestats($|\s.*)', self._hype_stats_command) bot.register_command(r'(?i)!klappa($|\s.*)', self._klappa_command) bot.register_command(r'(?i)!(mail|post)($|\s.*)$', self._mail_command) bot.register_command(r'(?i)!(mail|post)status($|\s.*)', self._mail_status_command) bot.register_command(r'(?i)!mute($|\s.*)', self._mute_command, ignore_rate_limit=True) bot.register_command(r'(?i)!normalize($|\s.*)', self._normalize_command) bot.register_command(r'(?i)!password\s+(.*)', self._password_command) bot.register_command(r'(?i)!pick\s+(.*)', self._pick_command) bot.register_command(r'(?i)!praise($|\s.{,100})$', self._praise_command) bot.register_command(r'(?i)!schedule($|\s.*)', self._schedule_command) bot.register_command(r'(?i)!(word)?(?:shuffle|scramble)($|\s.*)', self._shuffle_command) bot.register_command(r'(?i)!song($|\s.{,50})$', self._song_command) bot.register_command(r'(?i)!sort($|\s.*)', self._sort_command) bot.register_command(r'(?i)!spellchat(?:ot)?($|\s.*)', self._spell_chatot_command) bot.register_command(r'(?i)!racc(?:attack)?($|\s\S*)', self._raccattack_command) bot.register_command(r'(?i)!rand(?:om)?case($|\s.*)', self._rand_case_command) bot.register_command(r'(?i)!release($|\s.{,100})$', self._release_command) bot.register_command(r'(?i)!reverse($|\s.*)', self._reverse_command) bot.register_command(r'(?i)!riot($|\s.{,100})$', self._riot_command) bot.register_command(r'(?i)!rip($|\s.{,100})$', self._rip_command) bot.register_command(r'(?i)!roomsize?($|\s.*)', self._room_size_command) bot.register_command(r'(?i)!gen(?:erate)?match($|\s.*)$', self._generate_match_command) bot.register_command(r'(?i)!(xd|minglee)($|\s.*)', self._xd_command) # bot.register_command(r'(?i)!(set)?{}($|\s.*)'.format(username), self._username_command) # Temporary disabled. interferes with rate limit # bot.register_command(r'.*\b[xX][dD] +MingLee\b.*', self._xd_rand_command) bot.register_command(r'(?i)!(wow)($|\s.*)', self._wow_command) bot.register_command( r'(?i)(?:has )?(?:just )?donate(?:d|s)? [^0-9]{0,5}([0-9][0-9,.]*)', self._donation_trigger) bot.register_message_handler('join', self._join_callback) bot.register_message_handler('part', self._part_callback) self._reseed_rng_sched() self._token_notify_sched() self._discord_presence_sched() def _reseed_rng_sched(self): _reseed() _logger.debug('RNG reseeded') self._bot.scheduler.enter(300, 0, self._reseed_rng_sched) def _token_notify_sched(self): interval = self._token_notifier.notify(self._bot) if not interval: interval = 60 _logger.debug('Next token analysis interval %s', interval) self._bot.scheduler.enter(interval, 0, self._token_notify_sched) def _discord_presence_sched(self): unread_count = self._database.get_status_count('unread') if unread_count: game_text = 'Mail Delivery: {} unread'.format(unread_count) else: game_text = gen_roar() self._bot.set_discord_presence(game_text) self._bot.scheduler.enter(300, 0, self._discord_presence_sched) @classmethod def is_too_long(cls, text, max_byte_length=400): return len(text.encode('utf-8', 'replace')) > max_byte_length def _try_say_or_reply_too_long(self, formatted_text, session: InboundMessageSession): platform_name = session.get_platform_name() if platform_name == 'twitch' and self._bot.twitch_char_limit: max_length = 500 max_byte_length = 1800 elif platform_name == 'discord': max_length = 2000 max_byte_length = max_length * 4 else: max_length = 400 max_byte_length = 400 if len(formatted_text) > max_length or self.is_too_long( formatted_text, max_byte_length): session.reply(self.TOO_LONG_TEXT_TEMPLATE.format(gen_roar())) return False else: session.say(formatted_text) return True def _collect_recent_message(self, session: InboundMessageSession): if session.message['event_type'] in ('pubmsg', 'action'): channel = session.message['channel'] username = session.message['username'] our_username = session.client.get_nickname(lower=True) if username != our_username: self._recent_messages_for_regex[channel].append( session.message) if not session.message['text'].startswith('!'): self._last_message[channel] = session.message def _help_command(self, session: InboundMessageSession): session.reply('{} {}'.format(gen_roar(), self._help_text), escape_links=True) def _roar_command(self, session: InboundMessageSession): session.say('{} {} {}'.format(gen_roar(), gen_roar(), gen_roar().upper())) def _huffle_command(self, session: InboundMessageSession): session.say(random.choice(['卅(◕‿◕)卅', '卅( ◕‿◕)卅つ✂ (◕‿◕ )卅'])) def _hype_stats_command(self, session: InboundMessageSession): stats_filename = self._config.get('hype_stats_filename') if not stats_filename or \ stats_filename and not os.path.exists(stats_filename): session.reply('{} This command is currently unavailable!'.format( gen_roar())) return try: with open(stats_filename) as file: doc = json.load(file) text_1 = '[{duration}] Lines/sec {averages_str} ' \ '· Hints/sec {hint_averages_str}'.format( duration=doc['stats']['duration'], averages_str=doc['stats']['averages_str'], hint_averages_str=doc['stats']['hint_averages_str'], ) text_2 = 'Chat {chat_graph} · Hint {hint_graph}'.format( chat_graph=doc['stats']['chat_graph'], hint_graph=doc['stats']['hint_graph'], ) except (ValueError, KeyError, IndexError): _logger.exception('Error formatting stats') else: session.say(text_1) session.say(text_2) def _regex_command(self, session: InboundMessageSession): channel = session.message['channel'] if self._avoid_pikalaxbot and 'pikalaxbot' in self._user_list[channel]: session.skip_rate_limit = True return # Special split http://stackoverflow.com/a/21107911/1524507 parts = re.split(r'(?<!\\)/', session.match.group(1)) if not (2 <= len(parts) <= 3): return search_pattern = parts[0] replacement = parts[1].replace('\\/', '/') options = parts[2] if len(parts) == 3 else '' flags = 0 count = 1 if 'i' in options: flags |= re.IGNORECASE if 'g' in options: count = 0 try: pattern = re.compile(search_pattern, flags) except re.error as error: session.reply('{} {}!'.format(gen_roar(), error.args[0].title())) return for history_message in reversed( self._recent_messages_for_regex[session.message['channel']]): text = history_message['text'] channel = session.message['channel'] if text.startswith('s/') or text.startswith('!s/'): continue try: matched = self._regex_server.search(pattern, text) except RegexTimeout: _logger.warning('Regex DoS by %s on %s', session.message['username'], session.message['channel']) session.reply(gen_roar().upper()) return if matched: try: new_text = pattern.sub(replacement, text, count=count) except re.error as error: session.reply('{} {}!'.format(gen_roar(), error.args[0].title())) return if _random.random() < 0.05: new_text = gen_roar() fake_out = True else: fake_out = False formatted_text = '{user} wishes to {stacked}correct ' \ '{target_user}: {text}'.format( user=session.message['nick'], target_user=history_message['nick'], text=new_text, stacked='re' if history_message.get('stacked') else '', ) ok = self._try_say_or_reply_too_long(formatted_text, session) if not ok: return if not fake_out: stacked_message = copy.copy(history_message) stacked_message['text'] = new_text stacked_message['stacked'] = True self._recent_messages_for_regex[channel].append( stacked_message) return session.reply( '{} Your request does not apply to any recent messages!'.format( gen_roar())) def _caw_command(self, session: InboundMessageSession): words = session.match.group(1).split() text = words[0] if words else 'CAW' amount = _random.randint(1, 6) if amount == 1: formatted_text = '⋛⋋( ՞ਊ ՞)⋌⋚ {}'.format(text) else: formatted_text = text.join([' ⋛⋋( ՞ਊ ՞)⋌⋚ '] * amount) self._try_say_or_reply_too_long(formatted_text, session) def _countdown_command(self, session: InboundMessageSession): try: with open(self._config['event_data_file']) as file: doc = json.load(file) texts = [] for event_doc in doc['events']: event_name = event_doc['name'] date_now = arrow.get() date_event = arrow.get(event_doc['date']) time_delta = date_event - date_now if time_delta.total_seconds() >= 0: phrase_ago = 'starts in' else: time_delta = abs(time_delta) phrase_ago = 'started ago' months, days = divmod(time_delta.days, 30) minutes, seconds = divmod(time_delta.seconds, 60) hours, minutes = divmod(minutes, 60) texts.append('{} {} ' '{} {} {} {} {} {} {} {} {} {}'.format( event_name, phrase_ago, months, gettext.ngettext('month', 'months', months), days, gettext.ngettext('day', 'days', days), hours, gettext.ngettext('hour', 'hours', hours), minutes, gettext.ngettext('minute', 'minutes', minutes), seconds, gettext.ngettext('second', 'seconds', seconds), )) formatted_text = '{} {}'.format(gen_roar(), ' • '.join(texts)) except (OSError, LookupError, ValueError, TypeError): _logger.exception('No schedule') formatted_text = '{} Schedule not available'.format(gen_roar()) self._try_say_or_reply_too_long(formatted_text, session) def _debug_echo_command(self, session: InboundMessageSession): text = session.match.group(1).strip() formatted_text = '{} {} {}'.format( gen_roar(), self._censor_text(session, text), self._censor_text(session, text, extra_censor=True)) session.reply(formatted_text, escape_links=True) def _double_command(self, session: InboundMessageSession): text = session.match.group(2).strip() last_message = self._last_message.get(session.message['channel']) if not text and (session.match.group(1) or not last_message): text = 'ヽ༼ຈل͜ຈ༽ノ DOUBLE TEAM ヽ༼ຈل͜ຈ༽ノ' elif not text: text = last_message['text'] double_text = ''.join(char * 2 for char in text) formatted_text = '{} Doubled! {}'.format(gen_roar(), double_text) self._try_say_or_reply_too_long(formatted_text, session) def _greeting_command(self, session: InboundMessageSession): is_set = session.match.group(1) text = session.match.group(3).strip() if is_set: self._database.set_greeting( session.message['channel'], session.message['user_id'] or session.message['username'], text) session.reply('{} Greeting saved'.format(gen_roar())) else: greeting = self._database.get_greeting(session.message['channel']) if greeting: formatted_text = '{} {}'.format(gen_roar(), greeting) else: formatted_text = '{}'.format(gen_roar()) session.say(formatted_text, multiline=True) def _mute_command(self, session: InboundMessageSession): channel = session.message['channel'] if self._bot.channel_spam_limiter.is_ok(channel): self._bot.channel_spam_limiter.update(channel, offset=60) def _normalize_command(self, session: InboundMessageSession): text = session.match.group(1).strip() last_message = self._last_message.get(session.message['channel']) if not text and last_message: text = last_message['text'] elif not text: text = 'Groudonger' normalize_text = unicodedata.normalize('NFKC', text) formatted_text = '{} Normalized! {}'.format(gen_roar(), normalize_text) self._try_say_or_reply_too_long(formatted_text, session) def _password_command(self, session: InboundMessageSession): text = session.match.group(1).strip() if not text: text = 'Groudonger' if not self._password_api_limiter.is_ok('password'): time.sleep(2) self._password_api_limiter.update('password') hasher = hashlib.sha1() hasher.update(text.encode('utf8', 'replace')) digest = hasher.hexdigest().lower() result = 'Unable to check password.' try: response = requests.get( 'https://api.pwnedpasswords.com/range/{}'.format(digest[:5]), headers={'user-agent': 'chatbot383'}) if response.status_code == 200: if digest[5:] in response.text.lower(): result = '{} **is** a Pwned Password!'.format(text) else: result = '{} is not a Pwned Password'.format(text) except requests.exceptions.RequestException: _logger.exception('Password check error') formatted_text = '{} {}'.format(gen_roar(), result) self._try_say_or_reply_too_long(formatted_text, session) def _pick_command(self, session: InboundMessageSession): text = session.match.group(1).strip() if not text: text = 'heads,tails' result = _random.choice(text.split(',')).strip() formatted_text = '{} Picked! {}'.format(gen_roar(), result) self._try_say_or_reply_too_long(formatted_text, session) def _praise_command(self, session: InboundMessageSession): text = session.match.group(1).strip() if text: formatted_text = '{} Praise {}!'.format(gen_roar(), text) else: formatted_text = '{} Praise it! Raise it!'.format(gen_roar()) self._try_say_or_reply_too_long(formatted_text, session) def _schedule_command(self, session: InboundMessageSession): try: with open(self._config['event_data_file']) as file: doc = json.load(file) texts = [] for event_doc in doc['events']: texts.append('{} {}'.format(event_doc['name'], event_doc['schedule'])) formatted_text = '{} {}'.format(gen_roar(), ' • '.join(texts)) except (OSError, LookupError, ValueError, TypeError): _logger.exception('No schedule') formatted_text = '{} Schedule not available'.format(gen_roar()) self._try_say_or_reply_too_long(formatted_text, session) def _shuffle_command(self, session: InboundMessageSession): word_shuffle = bool(session.match.group(1)) text = session.match.group(2).strip() last_message = self._last_message.get(session.message['channel']) if not text and last_message: text = last_message['text'] elif not text: text = 'Groudonger' if word_shuffle: shuffle_list = text.split() sep = ' ' else: shuffle_list = list(text) sep = '' _random.shuffle(shuffle_list) shuffle_text = sep.join(shuffle_list).strip() formatted_text = '{} Shuffled! {}'.format(gen_roar(), shuffle_text) if self._config.get('ignore_slimo', False) and \ ('chatotdungeon' in session.message['channel'] or 'electricnet' in session.message['channel']) and \ session.message['username'].startswith('slimoleq'): pass else: self._try_say_or_reply_too_long(formatted_text, session) def _song_command(self, session: InboundMessageSession): limiter_key = ('song', session.message['channel']) if not self._spam_limiter.is_ok(limiter_key): return text = session.match.group(1).strip() if not text: text = 'Groudonger' formatted_text = self.DONGER_SONG_TEMPLATE.format(donger=text) self._try_say_or_reply_too_long(formatted_text, session) self._spam_limiter.update(limiter_key) def _sort_command(self, session: InboundMessageSession): text = session.match.group(1).strip() last_message = self._last_message.get(session.message['channel']) if not text and last_message: text = last_message['text'] elif not text: text = 'Groudonger' sorted_text = ''.join(sorted(text)).strip() formatted_text = '{} Sorted! {}'.format(gen_roar(), sorted_text) self._try_say_or_reply_too_long(formatted_text, session) def _raccattack_command(self, session: InboundMessageSession): text = session.match.group(1).strip() or session.message['nick'] text = text.lstrip("!/.$`") extra = _random.choice([ '', '!' * random.randint(1, 3), '?' * random.randint(1, 3), '.' * random.randint(2, 5) ]) if _random.randint(0, 1): formatted_text = ' {} RaccAttack {}'.format(text, extra) else: formatted_text = 'RaccAttack {} {}'.format(extra, text) if session.get_platform_name() == 'discord': formatted_text = formatted_text.replace( 'RaccAttack', '<:RaccAttack:273584139469324299>') self._try_say_or_reply_too_long(formatted_text, session) def _spell_chatot_command(self, session: InboundMessageSession): limiter_key = ('chatot', session.message['channel']) if not self._spam_limiter.is_ok(limiter_key): return text = session.match.group(1).strip() or 'I LIKE CHATOT' text = text[:len(self.CHATOT_TEMPLATE)] buf = io.StringIO() text_index = 0 for index, letter in enumerate(self.CHATOT_TEMPLATE): if index % 2 == 0: buf.write(letter) elif text_index < len(text) and letter.strip(): if text[text_index].strip(): buf.write(text[text_index]) else: buf.write(letter) text_index += 1 else: buf.write(letter) formatted_text = buf.getvalue() session.say(formatted_text, multiline=True) self._spam_limiter.update(limiter_key) def _rand_case_command(self, session: InboundMessageSession): text = session.match.group(1).strip() last_message = self._last_message.get(session.message['channel']) if not text and last_message: text = last_message['text'] elif not text: text = 'Groudonger' rand_case_text = ''.join( char.swapcase() if _random.randint(0, 1) else char for char in text) formatted_text = '{} Random case! {}'.format(gen_roar(), rand_case_text) self._try_say_or_reply_too_long(formatted_text, session) def _reverse_command(self, session: InboundMessageSession): text = session.match.group(1).strip() last_message = self._last_message.get(session.message['channel']) if not text and last_message: text = last_message['text'] elif not text: text = 'Groudonger' reversed_text = ''.join(reversed(text)) formatted_text = '{} Reversed! {}'.format(gen_roar(), reversed_text) self._try_say_or_reply_too_long(formatted_text, session) def _release_command(self, session: InboundMessageSession): text = session.match.group(1).strip() or session.message['nick'] formatted_text = \ '{roar} {text} was released. Farewell, {text}!'.format( roar=gen_roar(), text=text ) self._try_say_or_reply_too_long(formatted_text, session) def _riot_command(self, session: InboundMessageSession): text = session.match.group(1).strip() if text: formatted_text = '{} {} or riot! {}'\ .format(gen_roar(), text, gen_roar().upper()) else: formatted_text = \ '{} {} {}'.format( gen_roar(), _random.choice(( 'Riot, I say! Riot, you may!', 'Riot!', '{} riot!'.format(session.message['nick']), 'Groudonger riot!', )), gen_roar().upper() ) self._try_say_or_reply_too_long(formatted_text, session) def _rip_command(self, session: InboundMessageSession): text = session.match.group(1).strip() or session.message['nick'] formatted_text = \ '{} {}, {}. Press F to pay your respects.'.format( gen_roar(), _random.choice(('RIP', 'Rest in peace')), text ) self._try_say_or_reply_too_long(formatted_text, session) def _room_size_command(self, session: InboundMessageSession): formatted_text = \ '{} {} users in chat room.'.format( gen_roar(), len(self._user_list[session.message['channel']]) ) self._try_say_or_reply_too_long(formatted_text, session) def _klappa_command(self, session: InboundMessageSession): session.say('{}'.format(_random.choice(('Kappa //', gen_roar())))) def _xd_command(self, session): num = random.randint(0, 2) if num == 0: formatted_text = '{} xD MingLee'.format(gen_roar().lower().replace( '!', '?')) elif num == 1: formatted_text = '{} xD MingLee'.format(gen_roar()) else: formatted_text = 'xD MingLee' session.say(formatted_text) def _xd_rand_command(self, session: InboundMessageSession): if _random.random() < 0.1 or \ session.message['username'] == 'wow_deku_onehand' and \ session.message['text'].strip() == 'xD MingLee': def rep_func(match): return '!' if _random.random() < 0.6 else '1' session.say('{} xD MingLee'.format( re.sub('!', rep_func, gen_roar().lower()))) def _wow_command(self, session: InboundMessageSession): if self._tellnext_generator: platform_name = session.get_platform_name() max_len = 500 if platform_name == 'discord' else 400 session.say('> {}'.format( self._censor_text( session, self._tellnext_generator.get_paragraph(max_len))), multiline=True) else: session.reply('{} Feature not available!'.format(gen_roar())) def _donation_trigger(self, session: InboundMessageSession): points = session.match.group(1).replace('.', '').replace(',', '') + '0' minutes = _random.randint(20, 1200) / 10 formatted_text = '{} {} {} {}'.format( _random.choice([ 'Thanks for donating!', 'Thank you for your donation!', 'Thank you for donating!' ]), _random.choice([ 'You gained {} points.'.format(points), 'You now have {} points.'.format(points), 'Your balance is now {} points.'.format(points), ]), _random.choice([ 'Your donation will be read by the streamer in {} minutes.'. format(minutes), 'Your donation has been queued. ' '{} donations waiting to be read.'.format(int(minutes)), 'Please wait {} minutes for your donation to be processed.'. format(minutes), ]), _random.choice(['<3', '<3', ':)', ':)', ':o'])) session.reply(formatted_text) def _mail_command(self, session: InboundMessageSession): if session.message['channel'] in self._mail_disabled_channels: session.reply('{} My mail services cannot be used here.'.format( gen_roar().replace('!', '.'))) return mail_text = session.match.group(2).strip() if mail_text: self._send_mail(mail_text, session) else: self._read_mail(session) def _send_mail(self, mail_text: str, session: InboundMessageSession): if len(mail_text) > self.MAIL_MAX_LEN: session.reply('{} Your message is too burdensome! ' 'Send a concise version instead. ' '({}/{})'.format(gen_roar(), len(mail_text), self.MAIL_MAX_LEN)) return try: platform_name = session.get_platform_name() username = '******'.format(session.message['username'], session.message['user_id'] or '', platform_name) self._database.put_mail(username, mail_text, session.message['channel']) except SenderOutboxFullError: session.reply('{} How embarrassing! Your outbox is full!'.format( gen_roar())) except MailbagFullError: session.reply( '{} Incredulous! My mailbag is full! Read one instead!'.format( gen_roar())) else: session.reply('Tremendous! I will deliver this mail to the next ' 'recipient without fail! {}'.format(gen_roar())) def _read_mail(self, session: InboundMessageSession): platform_name = session.get_platform_name() if _random.random() < 0.95: skip_username = session.message['username'] skip_user_id = '{}@{}'.format(session.message['user_id'], platform_name) else: skip_username = None skip_user_id = None channel = None if session.message['channel'] in self._config.get( 'mail_restricted_channels', ()): channel = session.message['channel'] _logger.debug('Restricting mail to channel %s', channel) mail_info = None if _random.random() < 0.3: mail_info = self._database.get_old_mail( skip_username=skip_username, skip_user_id=skip_user_id, channel=channel) if not mail_info: mail_info = self._database.get_mail(skip_username=skip_username, skip_user_id=skip_user_id) if not mail_info and _random.random() < 0.6: mail_info = self._database.get_old_mail() if not mail_info: session.reply( '{} Outlandish! There is no new mail! You should send some!'. format(gen_roar())) else: # From Discord proxy, space is escaped to \s and # lowercase to |s via IRC rules username = mail_info['username'].split('!', 1)[0]\ .replace('|s', ' ').title() username_extra = '' sender_platform = mail_info['username'].partition( '@')[-1] or 'twitch' if platform_name != sender_platform: username_extra = ' ({})'.format(sender_platform.title()) session.reply( '{roar} I am delivering mail! ' 'Here it is, {date}, from {username}{username_extra}: {msg}'. format(roar=gen_roar(), username=username, username_extra=username_extra, date=arrow.get(mail_info['timestamp']).humanize(), msg=self._censor_text( session, mail_info['text'], extra_censor=True if channel else False)), multiline=True, escape_links=True, ) def _mail_status_command(self, session: InboundMessageSession): unread_count = self._database.get_status_count('unread') read_count = self._database.get_status_count('read') channel = session.message['channel'] disabled = channel in self._mail_disabled_channels restricted = channel in self._config.get('mail_restricted_channels', ()) session.reply( '{roar} {unread} unread, {read} read, {total} total! ' '(Channel={channel}, Disabled={disabled}, Restricted={restricted})' .format( roar=gen_roar(), unread=unread_count, read=read_count, total=unread_count + read_count, channel=channel, disabled=disabled, restricted=restricted, )) def _generate_match_command(self, session: InboundMessageSession): if not self._match_generator: session.reply('{} Feature not available!'.format(gen_roar())) return args = session.match.group(1).lower().split() try: session.reply('{} {}'.format( gen_roar(), self._match_generator.get_match_string(args))) except MatchError as error: session.reply('{} An error generating a match: {}'.format( gen_roar(), error)) except (ValueError, IndexError, TypeError): _logger.exception('Generate match error') session.reply( '{} An error occurred when generating a match!'.format( gen_roar())) def _join_callback(self, session: InboundMessageSession): usernames = self._user_list[session.message['channel']] username = session.message['username'] usernames.add(username) def _part_callback(self, session: InboundMessageSession): usernames = self._user_list[session.message['channel']] username = session.message['username'] if username in usernames: usernames.remove(username) def _censor_text(self, session: InboundMessageSession, text: str, extra_censor: bool = False) -> str: if session.get_platform_name() == 'discord': text = chatbot383.censor.censor_text(text).replace( '***', '\\*\\*\\*') if extra_censor: text = chatbot383.censor.censor_link(text) return text
class Features(object): DONGER_SONG_TEMPLATE = ( 'I like to raise my {donger} I do it all the time ヽ༼ຈل͜ຈ༽ノ ' 'and every time its lowered┌༼ຈل͜ຈ༽┐ ' 'I cry and start to whine ┌༼@ل͜@༽┐' 'But never need to worry ༼ ºل͟º༽ ' 'my {donger}\'s staying strong ヽ༼ຈل͜ຈ༽ノ' 'A {donger} saved is a {donger} earned so sing the {donger} song!' ) TOO_LONG_TEXT_TEMPLATE = '{} Message length exceeds my capabilities!' MAIL_MAX_LEN = 300 FOOD_LOG_FILENAME = 'foodlog.txt' def __init__(self, bot, help_text, database, config, alert_channels=None): self._bot = bot self._help_text = help_text self._database = database self._config = config self._recent_messages_for_regex = collections.defaultdict(lambda: collections.deque(maxlen=100)) self._last_message = {} self._spam_limiter = Limiter(min_interval=10) self._regex_server = RegexServer() #self._token_notifier = TokenNotifier( # config.get('token_notify_filename'), # config.get('token_notify_channels'), # config.get('token_notify_interval', 60) #) self._tellnext_generator = None self._alert_channels = frozenset(alert_channels or ()) self._food_current = "" self._food_next = "" self._food_current_updated = None self._food_next_updated = None self._mail_disabled_channels = config.get('mail_disabled_channels') # self._tellnext_generator = TellnextGenerator(config['tellnext_database']) bot.register_message_handler('pubmsg', self._collect_recent_message) bot.register_message_handler('action', self._collect_recent_message) #bot.register_command(r's/(.+/.*)', self._regex_command) #bot.register_command(r'(?i)!double(team)?($|\s.*)', self._double_command) bot.register_command(r'(?i)!(groudonger)?help($|\s.*)', self._help_command) bot.register_command(r'(?i)!groudon(ger)?($|\s.*)', self._roar_command) #bot.register_command(r'(?i)!hypestats($|\s.*)', self._hype_stats_command) bot.register_command(r'(?i)!klappa($|\s.*)', self._klappa_command) #bot.register_command(r'(?i)!(mail|post)($|\s.*)$', self._mail_command) #bot.register_command(r'(?i)!(mail|post)status($|\s.*)', self._mail_status_command) #bot.register_command(r'(?i)!pick\s+(.*)', self._pick_command) #bot.register_command(r'(?i)!praise($|\s.{,100})$', self._praise_command) #bot.register_command(r'(?i)!(?:shuffle|scramble)($|\s.*)', self._shuffle_command) #bot.register_command(r'(?i)!song($|\s.{,50})$', self._song_command) #bot.register_command(r'(?i)!sort($|\s.*)', self._sort_command) #bot.register_command(r'(?i)!rand(?:om)?case($|\s.*)', self._rand_case_command) #bot.register_command(r'(?i)!release($|\s.{,100})$', self._release_command) #bot.register_command(r'(?i)!riot($|\s.{,100})$', self._riot_command) #bot.register_command(r'(?i)!rip($|\s.{,100})$', self._rip_command) #bot.register_command(r'(?i)!gen(?:erate)?match($|\s.*)$', self._generate_match_command) #bot.register_command(r'(?i)!(xd|minglee|chfoo)($|\s.*)', self._xd_command) # Temporary disabled. interferes with rate limit # bot.register_command(r'.*\b[xX][dD] +MingLee\b.*', self._xd_rand_command) #bot.register_command(r'(?i)!(wow)($|\s.*)', self._wow_command) bot.register_command(r'(?i)!foodcurrent($|\s.*)', self._food_current_command) bot.register_command(r'(?i)!foodnext($|\s.*)', self._food_next_command) self._reseed_rng_sched() #self._token_notify_sched() def _reseed_rng_sched(self): _reseed() _logger.debug('RNG reseeded') self._bot.scheduler.enter(300, 0, self._reseed_rng_sched) def _token_notify_sched(self): interval = self._token_notifier.notify(self._bot) if not interval: interval = 60 _logger.debug('Next token analysis interval %s', interval) self._bot.scheduler.enter(interval, 0, self._token_notify_sched) @classmethod def is_too_long(cls, text): return len(text.encode('utf-8', 'replace')) > 400 @classmethod def _try_say_or_reply_too_long(cls, formatted_text, session): if cls.is_too_long(formatted_text): session.reply(cls.TOO_LONG_TEXT_TEMPLATE.format(gen_roar())) return False else: session.say(formatted_text) return True def _collect_recent_message(self, session): if session.message['event_type'] in ('pubmsg', 'action'): channel = session.message['channel'] username = session.message['username'] our_username = session.client.get_nickname(lower=True) if username != our_username: #self._recent_messages_for_regex[channel].append(session.message) #if not session.message['text'].startswith('!'): # self._last_message[channel] = session.message if username.lower() == "food" and channel.lower() == "#food": self._collect_food_message(session.message) def _help_command(self, session): session.reply('{} {}'.format(gen_roar(), self._help_text)) def _roar_command(self, session): session.say('{} {} {}'.format(gen_roar(), gen_roar(), gen_roar().upper())) def _hype_stats_command(self, session): stats_filename = self._config.get('hype_stats_filename') if not stats_filename or \ stats_filename and not os.path.exists(stats_filename): session.reply( '{} This command is currently unavailable!'.format(gen_roar())) return try: with open(stats_filename) as file: doc = json.load(file) text_1 = '[{duration}] Lines/sec {averages_str} ' \ '· Hints/sec {hint_averages_str}'.format( duration=doc['stats']['duration'], averages_str=doc['stats']['averages_str'], hint_averages_str=doc['stats']['hint_averages_str'], ) text_2 = 'Chat {chat_graph} · Hint {hint_graph}'.format( chat_graph=doc['stats']['chat_graph'], hint_graph=doc['stats']['hint_graph'], ) except (ValueError, KeyError, IndexError): _logger.exception('Error formatting stats') else: session.say(text_1) session.say(text_2) def _regex_command(self, session): # Special split http://stackoverflow.com/a/21107911/1524507 parts = re.split(r'(?<!\\)/', session.match.group(1)) if not (2 <= len(parts) <= 3): return search_pattern = parts[0] replacement = parts[1] options = parts[2] if len(parts) == 3 else '' flags = 0 count = 1 if 'i' in options: flags |= re.IGNORECASE if 'g' in options: count = 0 try: pattern = re.compile(search_pattern, flags) except re.error as error: session.reply('{} {}!'.format(gen_roar(), error.args[0].title())) return for history_message in reversed(self._recent_messages_for_regex[session.message['channel']]): text = history_message['text'] channel = session.message['channel'] if text.startswith('s/'): continue try: matched = self._regex_server.search(pattern, text) except RegexTimeout: _logger.warning( 'Regex DoS by %s on %s', session.message['username'], session.message['channel']) session.reply(gen_roar().upper()) return if matched: try: new_text = pattern.sub(replacement, text, count=count) except re.error as error: session.reply('{} {}!'.format(gen_roar(), error.args[0].title())) return if _random.random() < 0.1: new_text = gen_roar() fake_out = True else: fake_out = False formatted_text = '{user} wishes to {stacked}correct ' \ '{target_user}: {text}'.format( user=session.message['nick'], target_user=history_message['nick'], text=new_text, stacked='re' if history_message.get('stacked') else '', ) ok = self._try_say_or_reply_too_long(formatted_text, session) if not ok: return if not fake_out: stacked_message = copy.copy(history_message) stacked_message['text'] = new_text stacked_message['stacked'] = True self._recent_messages_for_regex[channel].append(stacked_message) return session.reply('{} Your request does not apply to any recent messages!' .format(gen_roar())) def _double_command(self, session): text = session.match.group(2).strip() last_message = self._last_message.get(session.message['channel']) if not text and (session.match.group(1) or not last_message): text = 'ヽ༼ຈل͜ຈ༽ノ DOUBLE TEAM ヽ༼ຈل͜ຈ༽ノ' elif not text: text = last_message['text'] double_text = ''.join(char * 2 for char in text) formatted_text = '{} Doubled! {}'.format(gen_roar(), double_text) self._try_say_or_reply_too_long(formatted_text, session) def _pick_command(self, session): text = session.match.group(1).strip() if not text: text = 'heads,tails' result = _random.choice(text.split(',')).strip() formatted_text = '{} Picked! {}'.format(gen_roar(), result) self._try_say_or_reply_too_long(formatted_text, session) def _praise_command(self, session): text = session.match.group(1).strip() if text: formatted_text = '{} Praise {}!'.format(gen_roar(), text) else: formatted_text = '{} Praise it! Raise it!'.format(gen_roar()) self._try_say_or_reply_too_long(formatted_text, session) def _shuffle_command(self, session): text = session.match.group(1).strip() last_message = self._last_message.get(session.message['channel']) if not text and last_message: text = last_message['text'] elif not text: text = 'Groudonger' shuffle_list = list(text) _random.shuffle(shuffle_list) shuffle_text = ''.join(shuffle_list).strip() formatted_text = '{} Shuffled! {}'.format(gen_roar(), shuffle_text) self._try_say_or_reply_too_long(formatted_text, session) def _song_command(self, session): limiter_key = ('song', session.message['channel']) if not self._spam_limiter.is_ok(limiter_key): return text = session.match.group(1).strip() if not text: text = 'Groudonger' formatted_text = self.DONGER_SONG_TEMPLATE.format(donger=text) self._try_say_or_reply_too_long(formatted_text, session) self._spam_limiter.update(limiter_key) def _sort_command(self, session): text = session.match.group(1).strip() last_message = self._last_message.get(session.message['channel']) if not text and last_message: text = last_message['text'] elif not text: text = 'Groudonger' sorted_text = ''.join(sorted(text)).strip() formatted_text = '{} Sorted! {}'.format(gen_roar(), sorted_text) self._try_say_or_reply_too_long(formatted_text, session) def _rand_case_command(self, session): text = session.match.group(1).strip() last_message = self._last_message.get(session.message['channel']) if not text and last_message: text = last_message['text'] elif not text: text = 'Groudonger' rand_case_text = ''.join( char.swapcase() if _random.randint(0, 1) else char for char in text ) formatted_text = '{} Random case! {}'.format(gen_roar(), rand_case_text) self._try_say_or_reply_too_long(formatted_text, session) def _release_command(self, session): text = session.match.group(1).strip() or session.message['nick'] formatted_text = \ '{roar} {text} was released. Farewell, {text}!'.format( roar=gen_roar(), text=text ) self._try_say_or_reply_too_long(formatted_text, session) def _riot_command(self, session): text = session.match.group(1).strip() if text: formatted_text = '{} {} or riot! {}'\ .format(gen_roar(), text, gen_roar().upper()) else: formatted_text = \ '{} {} {}'.format( gen_roar(), _random.choice(( 'Riot, I say! Riot, you may!', 'Riot!', '{} riot!'.format(session.message['nick']), 'Groudonger riot!', )), gen_roar().upper() ) self._try_say_or_reply_too_long(formatted_text, session) def _rip_command(self, session): text = session.match.group(1).strip() or session.message['nick'] formatted_text = \ '{} {}, {}. Press F to pay your respects.'.format( gen_roar(), _random.choice(('RIP', 'Rest in peace')), text ) self._try_say_or_reply_too_long(formatted_text, session) def _klappa_command(self, session): session.say('{}'.format(_random.choice(('Kappa //', gen_roar())))) def _xd_command(self, session): session.say('{} xD MingLee'.format( gen_roar().lower().replace('!', '?')) ) def _xd_rand_command(self, session): if _random.random() < 0.1 or \ session.message['username'] == 'wow_deku_onehand' and \ session.message['text'].strip() == 'xD MingLee': def rep_func(match): return '!' if _random.random() < 0.6 else '1' session.say('{} xD MingLee'.format( re.sub('!', rep_func, gen_roar().lower())) ) def _wow_command(self, session): if self._tellnext_generator: session.say('> {}'.format(self._tellnext_generator.get_paragraph())) else: session.reply('{} Feature not available!'.format(gen_roar())) def _mail_command(self, session): if session.message['channel'] in self._mail_disabled_channels: session.reply( '{} My mail services cannot be used here.' .format(gen_roar().replace('!', '.')) ) return mail_text = session.match.group(2).strip() if mail_text: if len(mail_text) > self.MAIL_MAX_LEN: session.reply( '{} Your message is too burdensome! ' 'Send a concise version instead. ' '({}/{})' .format(gen_roar(), len(mail_text), self.MAIL_MAX_LEN) ) return try: self._database.put_mail(session.message['username'], mail_text) except SenderOutboxFullError: session.reply( '{} How embarrassing! Your outbox is full!' .format(gen_roar())) except MailbagFullError: session.reply( '{} Incredulous! My mailbag is full! Read one instead!' .format(gen_roar())) else: session.reply( 'Tremendous! I will deliver this mail to the next ' 'recipient without fail! {}'.format(gen_roar())) else: if _random.random() < 0.3: mail_info = self._database.get_old_mail() else: if _random.random() < 0.7: skip_username = session.message['username'] else: skip_username = None mail_info = self._database.get_mail(skip_username=skip_username) if not mail_info and _random.random() < 0.3: mail_info = self._database.get_old_mail() if not mail_info: session.reply( '{} Outlandish! There is no new mail! You should send some!' .format(gen_roar()) ) else: session.reply( '{roar} I am delivering mail! ' 'Here it is, {date}, from {username}: {msg}' .format( roar=gen_roar(), username=mail_info['username'].title(), date=arrow.get(mail_info['timestamp']).humanize(), msg=mail_info['text']), multiline=True ) def _mail_status_command(self, session): unread_count = self._database.get_status_count('unread') read_count = self._database.get_status_count('read') session.reply( '{roar} {unread} unread, {read} read, {total} total!'.format( roar=gen_roar(), unread=unread_count, read=read_count, total=unread_count + read_count ) ) def _alert_important_food_change(self, title, isNow): # This is only called if the title is different from what we already had in our variables if re.search(r"\bjulia\b", title, re.I) or re.search(r"\bfrench\s+chef\b", title, re.I): if isNow: out = 'twitch.tv/food is now playing "{}"'.format(title) else: out = 'twitch.tv/food will play "{}" next'.format(title) for channel in self._alert_channels: self._bot.send_text(channel, 'PogChamp {}'.format(out)) def _log_food_nowplaying(self): output = "{}: {}\n".format(self._food_current_updated.isoformat(), self._food_current) with open(self.FOOD_LOG_FILENAME, "a") as logfile: logfile.write(output) def _collect_food_message(self, message): text = message['text'] # What's next whats_next = "" match = re.fullmatch(r"\s*will play \"([^\"]+)\" next\s*", text, re.I) if match and self._food_next != "voting": # This particular format is invalid when voting whats_next = match.group(1) match = re.fullmatch(r"\s*the vote is done! the winner is \"([^\"]+)\" with [0-9]+ votes?\s*", text, re.I) if match: whats_next = match.group(1) if whats_next and self._food_next != whats_next: self._alert_important_food_change(whats_next, False) self._food_next = whats_next self._food_next_updated = datetime.datetime.now(datetime.timezone.utc) # What's playing match = re.fullmatch(r"\s*now playing \"([^\"]+)\"\s*", text, re.I) if match: whats_on = match.group(1) if whats_on and self._food_current != whats_on: self._alert_important_food_change(whats_on, True) self._food_current = whats_on self._food_current_updated = datetime.datetime.now(datetime.timezone.utc) self._log_food_nowplaying() # If this was previously "next", then now we don't know what "next" is. if self._food_next == self._food_current: self._food_next = "" self._food_next_updated = datetime.datetime.now(datetime.timezone.utc) # Voting in progress if text == "TIME TO VOTE!": whats_next = "voting" if self._food_next != whats_next: self._food_next = whats_next self._food_next_updated = datetime.datetime.now(datetime.timezone.utc) def _food_updated_string(self, dt): if not dt: return "" now = datetime.datetime.now(datetime.timezone.utc) rd = dateutil.relativedelta.relativedelta(now, dt) attrs = ['years', 'months', 'days', 'hours', 'minutes', 'seconds'] human_readable = lambda delta: [ '%d %s' % (getattr(delta, attr), getattr(delta, attr) > 1 and attr or attr[:-1]) for attr in attrs if getattr(delta, attr) ] hr = human_readable(rd) if len(hr) > 0: return "(last updated {} ago)".format(hr[0]) return "" def _food_current_command(self, session): updated_string = self._food_updated_string(self._food_current_updated) if not self._food_current: session.reply( '{} I have no idea what\'s playing on twitch.tv/food right now! :( {}' .format(gen_roar(), updated_string) ) else: session.reply( '{roar} twitch.tv/food is now playing "{title}"! {updated}' .format( roar=gen_roar(), title=self._food_current, updated=updated_string ) ) def _food_next_command(self, session): updated_string = self._food_updated_string(self._food_next_updated) if not self._food_next: session.reply( '{} I have no idea what\'s playing on twitch.tv/food next! :( {}' .format(gen_roar(), updated_string) ) else: title = self._food_next if title == "voting": message = "twitch.tv/food chat is currently voting for what to play next." else: message = 'twitch.tv/food will play "{}" next!'.format(title) session.reply('{} {} {}'.format(gen_roar(), message, updated_string))
class Features(object): DONGER_SONG_TEMPLATE = ( 'I like to raise my {donger} I do it all the time ヽ༼ຈل͜ຈ༽ノ ' 'and every time its lowered┌༼ຈل͜ຈ༽┐ ' 'I cry and start to whine ┌༼@ل͜@༽┐' 'But never need to worry ༼ ºل͟º༽ ' 'my {donger}\'s staying strong ヽ༼ຈل͜ຈ༽ノ' 'A {donger} saved is a {donger} earned so sing the {donger} song! ' 'ᕦ༼ຈل͜ຈ༽ᕤ') TOO_LONG_TEXT_TEMPLATE = '{} Message length exceeds my capabilities!' MAIL_MAX_LEN = 300 def __init__(self, bot: Bot, help_text: str, database: Database, config: configparser.ConfigParser): self._bot = bot self._help_text = help_text self._database = database self._config = config self._recent_messages_for_regex = collections.defaultdict( lambda: collections.deque(maxlen=100)) self._last_message = {} self._spam_limiter = Limiter(min_interval=10) self._user_list = collections.defaultdict(set) self._regex_server = RegexServer() self._token_notifier = TokenNotifier( config.get('token_notify_filename'), config.get('token_notify_channels'), config.get('token_notify_interval', 60)) self._tellnext_generator = None if os.path.isfile(config.get('tellnext_database', '')): self._tellnext_generator = TellnextGenerator( config['tellnext_database']) self._match_generator = None if os.path.isfile(config.get('veekun_pokedex_database', '')): self._match_generator = MatchGenerator( config['veekun_pokedex_database']) self._battlebot = BattleBot(config['veekun_pokedex_database'], self._bot) bot.register_message_handler('pubmsg', self._battlebot.message_callback) bot.register_message_handler('whisper', self._battlebot.message_callback) self._mail_disabled_channels = config.get('mail_disabled_channels') self._avoid_pikalaxbot = config.get('avoid_pikalaxbot') bot.register_message_handler('pubmsg', self._collect_recent_message) bot.register_message_handler('action', self._collect_recent_message) bot.register_command(r's/(.+/.*)', self._regex_command) bot.register_command(r'(?i)!double(team)?($|\s.*)', self._double_command) bot.register_command(r'(?i)!(groudonger)?(help|commands)($|\s.*)', self._help_command) bot.register_command(r'(?i)!groudon(ger)?($|\s.*)', self._roar_command) bot.register_command(r'(?i)!hypestats($|\s.*)', self._hype_stats_command) bot.register_command(r'(?i)!klappa($|\s.*)', self._klappa_command) bot.register_command(r'(?i)!(mail|post)($|\s.*)$', self._mail_command) bot.register_command(r'(?i)!(mail|post)status($|\s.*)', self._mail_status_command) bot.register_command(r'(?i)!mute($|\s.*)', self._mute_command, ignore_rate_limit=True) bot.register_command(r'(?i)!pick\s+(.*)', self._pick_command) bot.register_command(r'(?i)!praise($|\s.{,100})$', self._praise_command) bot.register_command(r'(?i)!(word)?(?:shuffle|scramble)($|\s.*)', self._shuffle_command) bot.register_command(r'(?i)!song($|\s.{,50})$', self._song_command) bot.register_command(r'(?i)!sort($|\s.*)', self._sort_command) bot.register_command(r'(?i)!rand(?:om)?case($|\s.*)', self._rand_case_command) bot.register_command(r'(?i)!release($|\s.{,100})$', self._release_command) bot.register_command(r'(?i)!reverse($|\s.*)', self._reverse_command) bot.register_command(r'(?i)!riot($|\s.{,100})$', self._riot_command) bot.register_command(r'(?i)!rip($|\s.{,100})$', self._rip_command) bot.register_command(r'(?i)!roomsize?($|\s.*)', self._room_size_command) bot.register_command(r'(?i)!gen(?:erate)?match($|\s.*)$', self._generate_match_command) bot.register_command(r'(?i)!(xd|minglee)($|\s.*)', self._xd_command) # bot.register_command(r'(?i)!(set)?{}($|\s.*)'.format(username), self._username_command) # Temporary disabled. interferes with rate limit # bot.register_command(r'.*\b[xX][dD] +MingLee\b.*', self._xd_rand_command) bot.register_command(r'(?i)!(wow)($|\s.*)', self._wow_command) bot.register_message_handler('join', self._join_callback) bot.register_message_handler('part', self._part_callback) self._reseed_rng_sched() self._token_notify_sched() def _reseed_rng_sched(self): _reseed() _logger.debug('RNG reseeded') self._bot.scheduler.enter(300, 0, self._reseed_rng_sched) def _token_notify_sched(self): interval = self._token_notifier.notify(self._bot) if not interval: interval = 60 _logger.debug('Next token analysis interval %s', interval) self._bot.scheduler.enter(interval, 0, self._token_notify_sched) @classmethod def is_too_long(cls, text): return len(text.encode('utf-8', 'replace')) > 400 @classmethod def _try_say_or_reply_too_long(cls, formatted_text, session: InboundMessageSession): if cls.is_too_long(formatted_text): session.reply(cls.TOO_LONG_TEXT_TEMPLATE.format(gen_roar())) return False else: session.say(formatted_text) return True def _collect_recent_message(self, session: InboundMessageSession): if session.message['event_type'] in ('pubmsg', 'action'): channel = session.message['channel'] username = session.message['username'] our_username = session.client.get_nickname(lower=True) if username != our_username: self._recent_messages_for_regex[channel].append( session.message) if not session.message['text'].startswith('!'): self._last_message[channel] = session.message def _help_command(self, session: InboundMessageSession): session.reply('{} {}'.format(gen_roar(), self._help_text)) def _roar_command(self, session: InboundMessageSession): session.say('{} {} {}'.format(gen_roar(), gen_roar(), gen_roar().upper())) def _hype_stats_command(self, session: InboundMessageSession): stats_filename = self._config.get('hype_stats_filename') if not stats_filename or \ stats_filename and not os.path.exists(stats_filename): session.reply('{} This command is currently unavailable!'.format( gen_roar())) return try: with open(stats_filename) as file: doc = json.load(file) text_1 = '[{duration}] Lines/sec {averages_str} ' \ '· Hints/sec {hint_averages_str}'.format( duration=doc['stats']['duration'], averages_str=doc['stats']['averages_str'], hint_averages_str=doc['stats']['hint_averages_str'], ) text_2 = 'Chat {chat_graph} · Hint {hint_graph}'.format( chat_graph=doc['stats']['chat_graph'], hint_graph=doc['stats']['hint_graph'], ) except (ValueError, KeyError, IndexError): _logger.exception('Error formatting stats') else: session.say(text_1) session.say(text_2) def _regex_command(self, session: InboundMessageSession): channel = session.message['channel'] if self._avoid_pikalaxbot and 'pikalaxbot' in self._user_list[channel]: session.skip_rate_limit = True return # Special split http://stackoverflow.com/a/21107911/1524507 parts = re.split(r'(?<!\\)/', session.match.group(1)) if not (2 <= len(parts) <= 3): return search_pattern = parts[0] replacement = parts[1] options = parts[2] if len(parts) == 3 else '' flags = 0 count = 1 if 'i' in options: flags |= re.IGNORECASE if 'g' in options: count = 0 try: pattern = re.compile(search_pattern, flags) except re.error as error: session.reply('{} {}!'.format(gen_roar(), error.args[0].title())) return for history_message in reversed( self._recent_messages_for_regex[session.message['channel']]): text = history_message['text'] channel = session.message['channel'] if text.startswith('s/'): continue try: matched = self._regex_server.search(pattern, text) except RegexTimeout: _logger.warning('Regex DoS by %s on %s', session.message['username'], session.message['channel']) session.reply(gen_roar().upper()) return if matched: try: new_text = pattern.sub(replacement, text, count=count) except re.error as error: session.reply('{} {}!'.format(gen_roar(), error.args[0].title())) return if _random.random() < 0.05: new_text = gen_roar() fake_out = True else: fake_out = False formatted_text = '{user} wishes to {stacked}correct ' \ '{target_user}: {text}'.format( user=session.message['nick'], target_user=history_message['nick'], text=new_text, stacked='re' if history_message.get('stacked') else '', ) ok = self._try_say_or_reply_too_long(formatted_text, session) if not ok: return if not fake_out: stacked_message = copy.copy(history_message) stacked_message['text'] = new_text stacked_message['stacked'] = True self._recent_messages_for_regex[channel].append( stacked_message) return session.reply( '{} Your request does not apply to any recent messages!'.format( gen_roar())) def _double_command(self, session: InboundMessageSession): text = session.match.group(2).strip() last_message = self._last_message.get(session.message['channel']) if not text and (session.match.group(1) or not last_message): text = 'ヽ༼ຈل͜ຈ༽ノ DOUBLE TEAM ヽ༼ຈل͜ຈ༽ノ' elif not text: text = last_message['text'] double_text = ''.join(char * 2 for char in text) formatted_text = '{} Doubled! {}'.format(gen_roar(), double_text) self._try_say_or_reply_too_long(formatted_text, session) def _mute_command(self, session: InboundMessageSession): channel = session.message['channel'] if self._bot.channel_spam_limiter.is_ok(channel): self._bot.channel_spam_limiter.update(channel, offset=60) def _pick_command(self, session: InboundMessageSession): text = session.match.group(1).strip() if not text: text = 'heads,tails' result = _random.choice(text.split(',')).strip() formatted_text = '{} Picked! {}'.format(gen_roar(), result) self._try_say_or_reply_too_long(formatted_text, session) def _praise_command(self, session: InboundMessageSession): text = session.match.group(1).strip() if text: formatted_text = '{} Praise {}!'.format(gen_roar(), text) else: formatted_text = '{} Praise it! Raise it!'.format(gen_roar()) self._try_say_or_reply_too_long(formatted_text, session) def _shuffle_command(self, session: InboundMessageSession): word_shuffle = bool(session.match.group(1)) text = session.match.group(2).strip() last_message = self._last_message.get(session.message['channel']) if not text and last_message: text = last_message['text'] elif not text: text = 'Groudonger' if word_shuffle: shuffle_list = text.split() sep = ' ' else: shuffle_list = list(text) sep = '' _random.shuffle(shuffle_list) shuffle_text = sep.join(shuffle_list).strip() formatted_text = '{} Shuffled! {}'.format(gen_roar(), shuffle_text) self._try_say_or_reply_too_long(formatted_text, session) def _song_command(self, session: InboundMessageSession): limiter_key = ('song', session.message['channel']) if not self._spam_limiter.is_ok(limiter_key): return text = session.match.group(1).strip() if not text: text = 'Groudonger' formatted_text = self.DONGER_SONG_TEMPLATE.format(donger=text) self._try_say_or_reply_too_long(formatted_text, session) self._spam_limiter.update(limiter_key) def _sort_command(self, session: InboundMessageSession): text = session.match.group(1).strip() last_message = self._last_message.get(session.message['channel']) if not text and last_message: text = last_message['text'] elif not text: text = 'Groudonger' sorted_text = ''.join(sorted(text)).strip() formatted_text = '{} Sorted! {}'.format(gen_roar(), sorted_text) self._try_say_or_reply_too_long(formatted_text, session) def _rand_case_command(self, session: InboundMessageSession): text = session.match.group(1).strip() last_message = self._last_message.get(session.message['channel']) if not text and last_message: text = last_message['text'] elif not text: text = 'Groudonger' rand_case_text = ''.join( char.swapcase() if _random.randint(0, 1) else char for char in text) formatted_text = '{} Random case! {}'.format(gen_roar(), rand_case_text) self._try_say_or_reply_too_long(formatted_text, session) def _reverse_command(self, session: InboundMessageSession): text = session.match.group(1).strip() last_message = self._last_message.get(session.message['channel']) if not text and last_message: text = last_message['text'] elif not text: text = 'Groudonger' reversed_text = ''.join(reversed(text)) formatted_text = '{} Reversed! {}'.format(gen_roar(), reversed_text) self._try_say_or_reply_too_long(formatted_text, session) def _release_command(self, session: InboundMessageSession): text = session.match.group(1).strip() or session.message['nick'] formatted_text = \ '{roar} {text} was released. Farewell, {text}!'.format( roar=gen_roar(), text=text ) self._try_say_or_reply_too_long(formatted_text, session) def _riot_command(self, session: InboundMessageSession): text = session.match.group(1).strip() if text: formatted_text = '{} {} or riot! {}'\ .format(gen_roar(), text, gen_roar().upper()) else: formatted_text = \ '{} {} {}'.format( gen_roar(), _random.choice(( 'Riot, I say! Riot, you may!', 'Riot!', '{} riot!'.format(session.message['nick']), 'Groudonger riot!', )), gen_roar().upper() ) self._try_say_or_reply_too_long(formatted_text, session) def _rip_command(self, session: InboundMessageSession): text = session.match.group(1).strip() or session.message['nick'] formatted_text = \ '{} {}, {}. Press F to pay your respects.'.format( gen_roar(), _random.choice(('RIP', 'Rest in peace')), text ) self._try_say_or_reply_too_long(formatted_text, session) def _room_size_command(self, session: InboundMessageSession): formatted_text = \ '{} {} users in chat room.'.format( gen_roar(), len(self._user_list[session.message['channel']]) ) self._try_say_or_reply_too_long(formatted_text, session) def _klappa_command(self, session: InboundMessageSession): session.say('{}'.format(_random.choice(('Kappa //', gen_roar())))) def _xd_command(self, session): num = random.randint(0, 2) if num == 0: formatted_text = '{} xD MingLee'.format(gen_roar().lower().replace( '!', '?')) elif num == 1: formatted_text = '{} xD MingLee'.format(gen_roar()) else: formatted_text = 'xD MingLee' session.say(formatted_text) def _xd_rand_command(self, session: InboundMessageSession): if _random.random() < 0.1 or \ session.message['username'] == 'wow_deku_onehand' and \ session.message['text'].strip() == 'xD MingLee': def rep_func(match): return '!' if _random.random() < 0.6 else '1' session.say('{} xD MingLee'.format( re.sub('!', rep_func, gen_roar().lower()))) def _wow_command(self, session: InboundMessageSession): if self._tellnext_generator: session.say('> {}'.format( self._tellnext_generator.get_paragraph())) else: session.reply('{} Feature not available!'.format(gen_roar())) def _mail_command(self, session: InboundMessageSession): if session.message['channel'] in self._mail_disabled_channels: session.reply('{} My mail services cannot be used here.'.format( gen_roar().replace('!', '.'))) return mail_text = session.match.group(2).strip() if mail_text: if len(mail_text) > self.MAIL_MAX_LEN: session.reply('{} Your message is too burdensome! ' 'Send a concise version instead. ' '({}/{})'.format(gen_roar(), len(mail_text), self.MAIL_MAX_LEN)) return try: self._database.put_mail(session.message['username'], mail_text) except SenderOutboxFullError: session.reply( '{} How embarrassing! Your outbox is full!'.format( gen_roar())) except MailbagFullError: session.reply( '{} Incredulous! My mailbag is full! Read one instead!'. format(gen_roar())) else: session.reply( 'Tremendous! I will deliver this mail to the next ' 'recipient without fail! {}'.format(gen_roar())) else: if _random.random() < 0.3: mail_info = self._database.get_old_mail() else: if _random.random() < 0.7: skip_username = session.message['username'] else: skip_username = None mail_info = self._database.get_mail( skip_username=skip_username) if not mail_info and _random.random() < 0.3: mail_info = self._database.get_old_mail() if not mail_info: session.reply( '{} Outlandish! There is no new mail! You should send some!' .format(gen_roar())) else: session.reply( '{roar} I am delivering mail! ' 'Here it is, {date}, from {username}: {msg}'.format( roar=gen_roar(), username=mail_info['username'].title(), date=arrow.get(mail_info['timestamp']).humanize(), msg=mail_info['text']), multiline=True) def _mail_status_command(self, session: InboundMessageSession): unread_count = self._database.get_status_count('unread') read_count = self._database.get_status_count('read') session.reply( '{roar} {unread} unread, {read} read, {total} total!'.format( roar=gen_roar(), unread=unread_count, read=read_count, total=unread_count + read_count)) def _generate_match_command(self, session: InboundMessageSession): if not self._match_generator: session.reply('{} Feature not available!'.format(gen_roar())) return args = session.match.group(1).lower().split() try: session.reply('{} {}'.format( gen_roar(), self._match_generator.get_match_string(args))) except MatchError as error: session.reply('{} An error generating a match: {}'.format( gen_roar(), error)) except (ValueError, IndexError, TypeError): _logger.exception('Generate match error') session.reply( '{} An error occurred when generating a match!'.format( gen_roar())) def _join_callback(self, session: InboundMessageSession): usernames = self._user_list[session.message['channel']] username = session.message['username'] usernames.add(username) def _part_callback(self, session: InboundMessageSession): usernames = self._user_list[session.message['channel']] username = session.message['username'] if username in usernames: usernames.remove(username)
class Features(object): DONGER_SONG_TEMPLATE = ( "I like to raise my {donger} I do it all the time ヽ༼ຈل͜ຈ༽ノ " "and every time its lowered┌༼ຈل͜ຈ༽┐ " "I cry and start to whine ┌༼@ل͜@༽┐" "But never need to worry ༼ ºل͟º༽ " "my {donger}'s staying strong ヽ༼ຈل͜ຈ༽ノ" "A {donger} saved is a {donger} earned so sing the {donger} song! " "ᕦ༼ຈل͜ຈ༽ᕤ" ) TOO_LONG_TEXT_TEMPLATE = "{} Message length exceeds my capabilities!" MAIL_MAX_LEN = 300 def __init__(self, bot: Bot, help_text: str, database: Database, config: configparser.ConfigParser): self._bot = bot self._help_text = help_text self._database = database self._config = config self._recent_messages_for_regex = collections.defaultdict(lambda: collections.deque(maxlen=100)) self._last_message = {} self._spam_limiter = Limiter(min_interval=10) self._user_list = collections.defaultdict(set) self._regex_server = RegexServer() self._token_notifier = TokenNotifier( config.get("token_notify_filename"), config.get("token_notify_channels"), config.get("token_notify_interval", 60), ) self._tellnext_generator = None if os.path.isfile(config.get("tellnext_database", "")): self._tellnext_generator = TellnextGenerator(config["tellnext_database"]) self._match_generator = None if os.path.isfile(config.get("veekun_pokedex_database", "")): self._match_generator = MatchGenerator(config["veekun_pokedex_database"]) self._battlebot = BattleBot(config["veekun_pokedex_database"], self._bot) bot.register_message_handler("pubmsg", self._battlebot.message_callback) bot.register_message_handler("whisper", self._battlebot.message_callback) self._mail_disabled_channels = config.get("mail_disabled_channels") self._avoid_pikalaxbot = config.get("avoid_pikalaxbot") bot.register_message_handler("pubmsg", self._collect_recent_message) bot.register_message_handler("action", self._collect_recent_message) bot.register_command(r"s/(.+/.*)", self._regex_command) bot.register_command(r"(?i)!double(team)?($|\s.*)", self._double_command) bot.register_command(r"(?i)!(groudonger)?(help|commands)($|\s.*)", self._help_command) bot.register_command(r"(?i)!groudon(ger)?($|\s.*)", self._roar_command) bot.register_command(r"(?i)!hypestats($|\s.*)", self._hype_stats_command) bot.register_command(r"(?i)!klappa($|\s.*)", self._klappa_command) bot.register_command(r"(?i)!(mail|post)($|\s.*)$", self._mail_command) bot.register_command(r"(?i)!(mail|post)status($|\s.*)", self._mail_status_command) bot.register_command(r"(?i)!mute($|\s.*)", self._mute_command, ignore_rate_limit=True) bot.register_command(r"(?i)!pick\s+(.*)", self._pick_command) bot.register_command(r"(?i)!praise($|\s.{,100})$", self._praise_command) bot.register_command(r"(?i)!(word)?(?:shuffle|scramble)($|\s.*)", self._shuffle_command) bot.register_command(r"(?i)!song($|\s.{,50})$", self._song_command) bot.register_command(r"(?i)!sort($|\s.*)", self._sort_command) bot.register_command(r"(?i)!rand(?:om)?case($|\s.*)", self._rand_case_command) bot.register_command(r"(?i)!release($|\s.{,100})$", self._release_command) bot.register_command(r"(?i)!reverse($|\s.*)", self._reverse_command) bot.register_command(r"(?i)!riot($|\s.{,100})$", self._riot_command) bot.register_command(r"(?i)!rip($|\s.{,100})$", self._rip_command) bot.register_command(r"(?i)!roomsize?($|\s.*)", self._room_size_command) bot.register_command(r"(?i)!gen(?:erate)?match($|\s.*)$", self._generate_match_command) bot.register_command(r"(?i)!(xd|minglee)($|\s.*)", self._xd_command) # bot.register_command(r'(?i)!(set)?{}($|\s.*)'.format(username), self._username_command) # Temporary disabled. interferes with rate limit # bot.register_command(r'.*\b[xX][dD] +MingLee\b.*', self._xd_rand_command) bot.register_command(r"(?i)!(wow)($|\s.*)", self._wow_command) bot.register_message_handler("join", self._join_callback) bot.register_message_handler("part", self._part_callback) self._reseed_rng_sched() self._token_notify_sched() def _reseed_rng_sched(self): _reseed() _logger.debug("RNG reseeded") self._bot.scheduler.enter(300, 0, self._reseed_rng_sched) def _token_notify_sched(self): interval = self._token_notifier.notify(self._bot) if not interval: interval = 60 _logger.debug("Next token analysis interval %s", interval) self._bot.scheduler.enter(interval, 0, self._token_notify_sched) @classmethod def is_too_long(cls, text): return len(text.encode("utf-8", "replace")) > 400 @classmethod def _try_say_or_reply_too_long(cls, formatted_text, session: InboundMessageSession): if cls.is_too_long(formatted_text): session.reply(cls.TOO_LONG_TEXT_TEMPLATE.format(gen_roar())) return False else: session.say(formatted_text) return True def _collect_recent_message(self, session: InboundMessageSession): if session.message["event_type"] in ("pubmsg", "action"): channel = session.message["channel"] username = session.message["username"] our_username = session.client.get_nickname(lower=True) if username != our_username: self._recent_messages_for_regex[channel].append(session.message) if not session.message["text"].startswith("!"): self._last_message[channel] = session.message def _help_command(self, session: InboundMessageSession): session.reply("{} {}".format(gen_roar(), self._help_text)) def _roar_command(self, session: InboundMessageSession): session.say("{} {} {}".format(gen_roar(), gen_roar(), gen_roar().upper())) def _hype_stats_command(self, session: InboundMessageSession): stats_filename = self._config.get("hype_stats_filename") if not stats_filename or stats_filename and not os.path.exists(stats_filename): session.reply("{} This command is currently unavailable!".format(gen_roar())) return try: with open(stats_filename) as file: doc = json.load(file) text_1 = "[{duration}] Lines/sec {averages_str} " "· Hints/sec {hint_averages_str}".format( duration=doc["stats"]["duration"], averages_str=doc["stats"]["averages_str"], hint_averages_str=doc["stats"]["hint_averages_str"], ) text_2 = "Chat {chat_graph} · Hint {hint_graph}".format( chat_graph=doc["stats"]["chat_graph"], hint_graph=doc["stats"]["hint_graph"] ) except (ValueError, KeyError, IndexError): _logger.exception("Error formatting stats") else: session.say(text_1) session.say(text_2) def _regex_command(self, session: InboundMessageSession): channel = session.message["channel"] if self._avoid_pikalaxbot and "pikalaxbot" in self._user_list[channel]: session.skip_rate_limit = True return # Special split http://stackoverflow.com/a/21107911/1524507 parts = re.split(r"(?<!\\)/", session.match.group(1)) if not (2 <= len(parts) <= 3): return search_pattern = parts[0] replacement = parts[1] options = parts[2] if len(parts) == 3 else "" flags = 0 count = 1 if "i" in options: flags |= re.IGNORECASE if "g" in options: count = 0 try: pattern = re.compile(search_pattern, flags) except re.error as error: session.reply("{} {}!".format(gen_roar(), error.args[0].title())) return for history_message in reversed(self._recent_messages_for_regex[session.message["channel"]]): text = history_message["text"] channel = session.message["channel"] if text.startswith("s/"): continue try: matched = self._regex_server.search(pattern, text) except RegexTimeout: _logger.warning("Regex DoS by %s on %s", session.message["username"], session.message["channel"]) session.reply(gen_roar().upper()) return if matched: try: new_text = pattern.sub(replacement, text, count=count) except re.error as error: session.reply("{} {}!".format(gen_roar(), error.args[0].title())) return if _random.random() < 0.05: new_text = gen_roar() fake_out = True else: fake_out = False formatted_text = "{user} wishes to {stacked}correct " "{target_user}: {text}".format( user=session.message["nick"], target_user=history_message["nick"], text=new_text, stacked="re" if history_message.get("stacked") else "", ) ok = self._try_say_or_reply_too_long(formatted_text, session) if not ok: return if not fake_out: stacked_message = copy.copy(history_message) stacked_message["text"] = new_text stacked_message["stacked"] = True self._recent_messages_for_regex[channel].append(stacked_message) return session.reply("{} Your request does not apply to any recent messages!".format(gen_roar())) def _double_command(self, session: InboundMessageSession): text = session.match.group(2).strip() last_message = self._last_message.get(session.message["channel"]) if not text and (session.match.group(1) or not last_message): text = "ヽ༼ຈل͜ຈ༽ノ DOUBLE TEAM ヽ༼ຈل͜ຈ༽ノ" elif not text: text = last_message["text"] double_text = "".join(char * 2 for char in text) formatted_text = "{} Doubled! {}".format(gen_roar(), double_text) self._try_say_or_reply_too_long(formatted_text, session) def _mute_command(self, session: InboundMessageSession): channel = session.message["channel"] if self._bot.channel_spam_limiter.is_ok(channel): self._bot.channel_spam_limiter.update(channel, offset=60) def _pick_command(self, session: InboundMessageSession): text = session.match.group(1).strip() if not text: text = "heads,tails" result = _random.choice(text.split(",")).strip() formatted_text = "{} Picked! {}".format(gen_roar(), result) self._try_say_or_reply_too_long(formatted_text, session) def _praise_command(self, session: InboundMessageSession): text = session.match.group(1).strip() if text: formatted_text = "{} Praise {}!".format(gen_roar(), text) else: formatted_text = "{} Praise it! Raise it!".format(gen_roar()) self._try_say_or_reply_too_long(formatted_text, session) def _shuffle_command(self, session: InboundMessageSession): word_shuffle = bool(session.match.group(1)) text = session.match.group(2).strip() last_message = self._last_message.get(session.message["channel"]) if not text and last_message: text = last_message["text"] elif not text: text = "Groudonger" if word_shuffle: shuffle_list = text.split() sep = " " else: shuffle_list = list(text) sep = "" _random.shuffle(shuffle_list) shuffle_text = sep.join(shuffle_list).strip() formatted_text = "{} Shuffled! {}".format(gen_roar(), shuffle_text) self._try_say_or_reply_too_long(formatted_text, session) def _song_command(self, session: InboundMessageSession): limiter_key = ("song", session.message["channel"]) if not self._spam_limiter.is_ok(limiter_key): return text = session.match.group(1).strip() if not text: text = "Groudonger" formatted_text = self.DONGER_SONG_TEMPLATE.format(donger=text) self._try_say_or_reply_too_long(formatted_text, session) self._spam_limiter.update(limiter_key) def _sort_command(self, session: InboundMessageSession): text = session.match.group(1).strip() last_message = self._last_message.get(session.message["channel"]) if not text and last_message: text = last_message["text"] elif not text: text = "Groudonger" sorted_text = "".join(sorted(text)).strip() formatted_text = "{} Sorted! {}".format(gen_roar(), sorted_text) self._try_say_or_reply_too_long(formatted_text, session) def _rand_case_command(self, session: InboundMessageSession): text = session.match.group(1).strip() last_message = self._last_message.get(session.message["channel"]) if not text and last_message: text = last_message["text"] elif not text: text = "Groudonger" rand_case_text = "".join(char.swapcase() if _random.randint(0, 1) else char for char in text) formatted_text = "{} Random case! {}".format(gen_roar(), rand_case_text) self._try_say_or_reply_too_long(formatted_text, session) def _reverse_command(self, session: InboundMessageSession): text = session.match.group(1).strip() last_message = self._last_message.get(session.message["channel"]) if not text and last_message: text = last_message["text"] elif not text: text = "Groudonger" reversed_text = "".join(reversed(text)) formatted_text = "{} Reversed! {}".format(gen_roar(), reversed_text) self._try_say_or_reply_too_long(formatted_text, session) def _release_command(self, session: InboundMessageSession): text = session.match.group(1).strip() or session.message["nick"] formatted_text = "{roar} {text} was released. Farewell, {text}!".format(roar=gen_roar(), text=text) self._try_say_or_reply_too_long(formatted_text, session) def _riot_command(self, session: InboundMessageSession): text = session.match.group(1).strip() if text: formatted_text = "{} {} or riot! {}".format(gen_roar(), text, gen_roar().upper()) else: formatted_text = "{} {} {}".format( gen_roar(), _random.choice( ( "Riot, I say! Riot, you may!", "Riot!", "{} riot!".format(session.message["nick"]), "Groudonger riot!", ) ), gen_roar().upper(), ) self._try_say_or_reply_too_long(formatted_text, session) def _rip_command(self, session: InboundMessageSession): text = session.match.group(1).strip() or session.message["nick"] formatted_text = "{} {}, {}. Press F to pay your respects.".format( gen_roar(), _random.choice(("RIP", "Rest in peace")), text ) self._try_say_or_reply_too_long(formatted_text, session) def _room_size_command(self, session: InboundMessageSession): formatted_text = "{} {} users in chat room.".format( gen_roar(), len(self._user_list[session.message["channel"]]) ) self._try_say_or_reply_too_long(formatted_text, session) def _klappa_command(self, session: InboundMessageSession): session.say("{}".format(_random.choice(("Kappa //", gen_roar())))) def _xd_command(self, session): num = random.randint(0, 2) if num == 0: formatted_text = "{} xD MingLee".format(gen_roar().lower().replace("!", "?")) elif num == 1: formatted_text = "{} xD MingLee".format(gen_roar()) else: formatted_text = "xD MingLee" session.say(formatted_text) def _xd_rand_command(self, session: InboundMessageSession): if ( _random.random() < 0.1 or session.message["username"] == "wow_deku_onehand" and session.message["text"].strip() == "xD MingLee" ): def rep_func(match): return "!" if _random.random() < 0.6 else "1" session.say("{} xD MingLee".format(re.sub("!", rep_func, gen_roar().lower()))) def _wow_command(self, session: InboundMessageSession): if self._tellnext_generator: session.say("> {}".format(self._tellnext_generator.get_paragraph())) else: session.reply("{} Feature not available!".format(gen_roar())) def _mail_command(self, session: InboundMessageSession): if session.message["channel"] in self._mail_disabled_channels: session.reply("{} My mail services cannot be used here.".format(gen_roar().replace("!", "."))) return mail_text = session.match.group(2).strip() if mail_text: if len(mail_text) > self.MAIL_MAX_LEN: session.reply( "{} Your message is too burdensome! " "Send a concise version instead. " "({}/{})".format(gen_roar(), len(mail_text), self.MAIL_MAX_LEN) ) return try: self._database.put_mail(session.message["username"], mail_text) except SenderOutboxFullError: session.reply("{} How embarrassing! Your outbox is full!".format(gen_roar())) except MailbagFullError: session.reply("{} Incredulous! My mailbag is full! Read one instead!".format(gen_roar())) else: session.reply( "Tremendous! I will deliver this mail to the next " "recipient without fail! {}".format(gen_roar()) ) else: if _random.random() < 0.3: mail_info = self._database.get_old_mail() else: if _random.random() < 0.7: skip_username = session.message["username"] else: skip_username = None mail_info = self._database.get_mail(skip_username=skip_username) if not mail_info and _random.random() < 0.3: mail_info = self._database.get_old_mail() if not mail_info: session.reply("{} Outlandish! There is no new mail! You should send some!".format(gen_roar())) else: session.reply( "{roar} I am delivering mail! " "Here it is, {date}, from {username}: {msg}".format( roar=gen_roar(), username=mail_info["username"].title(), date=arrow.get(mail_info["timestamp"]).humanize(), msg=mail_info["text"], ), multiline=True, ) def _mail_status_command(self, session: InboundMessageSession): unread_count = self._database.get_status_count("unread") read_count = self._database.get_status_count("read") session.reply( "{roar} {unread} unread, {read} read, {total} total!".format( roar=gen_roar(), unread=unread_count, read=read_count, total=unread_count + read_count ) ) def _generate_match_command(self, session: InboundMessageSession): if not self._match_generator: session.reply("{} Feature not available!".format(gen_roar())) return args = session.match.group(1).lower().split() try: session.reply("{} {}".format(gen_roar(), self._match_generator.get_match_string(args))) except MatchError as error: session.reply("{} An error generating a match: {}".format(gen_roar(), error)) except (ValueError, IndexError, TypeError): _logger.exception("Generate match error") session.reply("{} An error occurred when generating a match!".format(gen_roar())) def _join_callback(self, session: InboundMessageSession): usernames = self._user_list[session.message["channel"]] username = session.message["username"] usernames.add(username) def _part_callback(self, session: InboundMessageSession): usernames = self._user_list[session.message["channel"]] username = session.message["username"] if username in usernames: usernames.remove(username)