async def pun(bot, message): """``` Say a pun. Usage: * /pun ```""" text = None async with aiohttp.ClientSession() as session: async with session.get(PUN_URL) as r: if r.status == 200: source = await r.read() soup = Bs(source, 'html.parser') section = soup.findAll('ul', attrs={'class': 'puns single'}) if len(section) > 0: text = section[0].li.find(text=True, recursive=False) else: LOG.error(f'Pun.pun: bad status: {r.status}') if text: return Message(message=text) else: LOG.info('something went wrong.') return Message(message='Sorry, but no puns were found.')
def _hs_find_card(data, search_key): base_image_url = 'https://art.hearthstonejson.com/v1/render/latest/enUS/512x/{0}.png' near_matches = [] for crd in data: if 'name' not in crd or 'id' not in crd: continue if crd['name'].lower() == search_key.lower(): return Message(message=base_image_url.format(crd['id']), cleanup_original=False, cleanup_self=False) if crd['name'].lower().startswith(search_key): near_matches.append(crd) if near_matches: image_url = base_image_url.format(near_matches[0].get('id')) return Message( message=f'Multiple matches found, showing first\n{image_url}', cleanup_original=False, cleanup_self=False) return Message(message=f'No match found for {search_key}', cleanup_original=False)
async def purge(bot, message): """``` Delete prior commands and messages from Senpai from a channel. Usage: * /purge N ```""" channel = message.channel count = message.content[6:].strip() if 'Direct Message' in str(channel): return Message('Cannot purge DMs') try: limit = int(count) except ValueError: return Message('non-numeric purge value') if limit < 1: return Message('non-positive purge value') if limit > 100: limit = 100 def delete_condition(msg): return msg.author.id == Ids.SENPAI_ID or msg.content.startswith('/') await channel.purge(limit=limit, check=delete_condition)
async def hs_update(bot, message): """``` Download the newest data set of Hearthstone cards. This command only works in specific MTG/Hearthstone channels. Usage: * /hs_update ```""" base_api_url = 'https://api.hearthstonejson.com/v1' error_message = Message(message='Error querying hearthstone api', cleanup_original=False) r = requests.get(base_api_url) if r.status_code != 200: LOG.error( f'error fetching versions from {base_api_url}, status_code={r.status_code}' ) return error_message versions = [] soup = Bs(r.content, 'html.parser') for a in soup.findAll('a'): href = a.get('href') try: pieces = href.split('/') if len(pieces) >= 3: version = int(pieces[2]) versions.append(version) except ValueError: pass if not versions: return error_message api_url = f'{base_api_url}/{max(versions)}/enUS/cards.json' r = requests.get(api_url) if r.status_code != 200: LOG.error( f'error fetching cards.json from {api_url}, status_code={r.status_code}' ) return error_message try: json_data = r.json() with open(HS_FILE, 'w') as f: json.dump(json_data, f) except json.JSONDecodeError: LOG.error('error parsing cards.json') return error_message return Message('Updated successfully')
def handle_direct_message(self, message, channel_id): msg = message['msg'].partition('@' + self.botname)[2].strip() if message["msg"].startswith('@' + self.botname) \ else message["msg"].strip() if len(msg) > 0: command = msg.split()[0].lower() # arguments = " ".join(msg.split()[1:]) user = User.from_message(message) attachments = message['attachments'] pass_message = Message(message_id=message["_id"], text=msg, chat=Chat(chat_id=channel_id), user=user, attachments=attachments, json=message) conversation = self.conversations.get(user.id) variants = self.button_variants.get(channel_id) pass_message.text = variants.get( pass_message.text, pass_message.text) if variants else pass_message.text if conversation is not None: # Зарегистрирован следующий шаг f, args, kwargs = conversation self.conversations.pop(user.id) f(pass_message, *args, **kwargs) else: # Следующий шаг не найден, обработка как обычно for cmd_list in self.commands: if command.lower() in cmd_list[0]: cmd_list[1](pass_message) return if not self.handle_auto_answer(message, self.direct_answers, channel_id): if self.handle_unknown is not None: self.handle_unknown(pass_message) else: self.send_message( '@' + user.username + ' :' + choice(self.unknown_command), channel_id) else: user = User.from_message(message) attachments = message['attachments'] pass_message = Message(message_id=message["_id"], text=msg, chat=Chat(chat_id=channel_id), user=user, attachments=attachments, json=message) self.handle_unknown(pass_message)
def _get_help(request, author, channel, command_types, base_command): # Generate list of all available commands commands = {} for command_type in command_types: for command, help_text in Command.HELP_TEXT[command_type].items(): commands[command] = help_text responses = [] if not request: # Compile default help text help_text = '```' help_text += f'Use /{base_command} <command> for more information about a command.\n\n' for command_type in command_types: # Skip reporting the clips, they make the help text too large if command_type == CommandType.CLIP: continue commands_list = ', '.join( sorted([ '/{0}'.format(command) for command in Command.HELP_TEXT[command_type] ])) help_text += f'{command_type.name.capitalize()} commands: \n{commands_list}\n\n' help_text += '```' responses.append( Message(message=help_text, channel=author, cleanup_original=False, cleanup_self=False)) else: if request not in commands: return Message(message='No {0} command'.format(request), channel=author, cleanup_original=False, cleanup_self=False) responses.append( Message(message=commands[request], channel=author, cleanup_original=False, cleanup_self=False)) if 'Direct Message' not in str(channel) and responses: responses.append(Message(message='Sent you a DM.')) return responses
async def hs(bot, message): """``` Search local Hearthstone data file for information about a specified card. This command only works in specific MTG/Hearthstone channels. Usage: * /hs card_name * inline {card_name} request ```""" if not os.path.exists(HS_FILE): return Message( 'No local Hearthstone data file found, run "/hs_update" to generate one', cleanup_original=False) with open(HS_FILE, 'r') as f: data = json.load(f) # Check for regex match matches_hs = re.findall(HS_REGEX, message.content) search_keys = matches_hs or [message.content[4:].strip()] responses = [] for search_key in search_keys: responses.append(_hs_find_card(data, search_key)) return responses
async def sounds(bot, message): """``` PM a list of commands for sound clips that Senpai can play. Usage: * /sounds ```""" bot_message = '\n'.join(sorted(['/' + clip for clip in clips])) bot_response = Message(message=bot_message, channel=message.author, cleanup_self=False, cleanup_original=False) if 'Direct Message' not in str(message.channel): bot_response = [bot_response, Message(message='Sent you a DM.')] return bot_response
async def count(bot, message): """``` Give the number of available sound clips. Usage: * /count ```""" return Message(message=str(len(clips)) + ' sound clips')
async def translate(bot, message): """``` Use jisho.org to translate the given text. Usage: * /translate <text> ```""" text = message.content[11:].strip() url = 'http://jisho.org/api/v1/search/words?' try: if text: url += f'keyword={text.replace(" ", "+")}' js = None # Fetch json from url async with aiohttp.ClientSession() as session: async with session.get(url) as r: if r.status == 200: js = await r.json() else: LOG.error(f'bad status, {r.status}') # Get translations from resulting data structure if js: data = js['data'] if data: japanese = data[0]['japanese'][0] english = data[0]['senses'][0]['english_definitions'] output = '' if 'word' in japanese: output += f'Japanese: {japanese["word"]}\n' if 'reading' in japanese: output += f'Reading: {japanese["reading"]}\n' if english: output += f'English: {", ".join(english)}' return Message(message=output) else: return Message(message='No translations found') else: return Message(message='Usage: /translate <text>') except Exception as e: LOG.error('Error translating', exc_info=e)
async def play_spyfall(author, mentions): global GAME_PLAYERS bot_responses = [] with open(SPYFALL_FILE, 'r') as f: roles = json.load(f) GAME_PLAYERS = mentions if author not in mentions: GAME_PLAYERS.append(author) # Pick a random location locations = list(roles.keys()) location = random.choice(locations) # Assign a spy players = GAME_PLAYERS spy = random.choice(players) players.remove(spy) locations_list = '\n\t'.join(sorted(locations)) bot_responses.append( Message(message= f'You are the spy!\nPotential Locations: \n\t{locations_list}', channel=spy, cleanup_original=False, cleanup_self=False)) # Assign roles to other players for player in players: role = random.choice(roles) bot_responses.append( Message( message= f'You are not the spy.\nLocation: {location}\nYour Role: {role}', channel=player, cleanup_original=False, cleanup_self=False)) roles.remove(role) bot_responses.append(Message(message='Game started')) return bot_responses
async def np(bot, message): """``` Print the currently playing song. Usages: * /np * /nowplaying * /playing ```""" voice_client = _get_client(bot, message.guild) if voice_client is None or not voice_client.is_playing(): return Message(message='Not playing any music right now') source = voice_client.source if source.url_link: return Message( message=f'Now playing: {source.title}\n{source.url_link}') else: return Message(message=f'Now playing: {source.title}')
async def hi(bot, message): """``` Senpai says hi to you by name, if she knows your name. Use /callme to set your name. Usage: * /hi ```""" author_id = str(message.author.id) if os.path.isfile(NAMES_FILE): with open(NAMES_FILE, 'r') as f: names = json.load(f) if author_id in names: return Message(message=f'Hi {names[author_id]}.') else: return Message(message='Hi. I don\'t have a name for you, use /callme <name> to pick your name.') else: with open(NAMES_FILE, 'w') as f: f.write('{}') return Message(message='Hi. I don\'t have a name for you, use /callme <name> to pick your name.')
async def update(bot, message): """``` Update pip libraries. Usage: * /update * /update <library> ```""" pieces = message.content.strip().split(' ') library = pieces[1] if len(pieces) > 1 else 'yt-dlp' command = f'sudo pip install -U {library}' subprocess.Popen(shlex.split(command)).communicate() return Message('Done')
async def spyfall_again(): """``` Starts a new game with the same players as a previous game, if one exists. Players will be PM'd their roles. Usage: * /spyfall_again ```""" if GAME_PLAYERS: response = await play_spyfall(None, None) return response else: return Message(message='No previous game found')
async def skip(bot, message): """``` Skip the currently playing song. Usage: * /skip ```""" voice_client = _get_client(bot, message.guild) if voice_client is None or not voice_client.is_playing(): return Message(message='Not playing any music right now') voice_client.stop() _play_next(voice_client, None)
async def play(bot, message): """``` Download and play a youtube video or soundcloud song. This command will bring Senpai to your current voice channel if she is not already in it. Usages: * /play youtube_link <count> -> Play the linked song <count> number of times * /play youtube_search_words <count> -> Play the first YT result <count> number of times * /play soundcloud_link <count> -> Play the linked song <count> number of times * /play -> Resume paused music ```""" # Requester must be in a voice channel to use '/play' command if message.author.voice is None or message.author.voice.channel is None: return Message(message='You are not in a voice channel') song_and_count = message.content[6:].strip() if not song_and_count: await _resume(bot, message) return {} voice_client = _get_client(bot, message.guild) # Bring Senpai to the voice channel if she's not already in it if voice_client is None or message.author.voice.channel != voice_client.channel: response = await summon(bot, message) if response: return response else: voice_client = _get_client(bot, message.guild) # See if a number of times to play ths song is specified pieces = song_and_count.split() count = 1 song = song_and_count if len(pieces) > 1: try: count = int(pieces[-1]) song = ' '.join(pieces[0:-1]) except ValueError: pass if count < 1: count = 1 elif count > 25: count = 25 asyncio.ensure_future(_download_and_play(bot, voice_client, song, count))
async def cat(bot, message): """``` Links a random picture of a cat. Usage: * /cat ```""" async with aiohttp.ClientSession() as session: async with session.get(CAT_URL) as r: if r.status == 200: js = await r.json() return Message(message=js['file']) else: LOG.error(f'Bad status: {r.status}')
async def queue(bot, message): """``` Lists the songs currently in the queue. Usage: * /queue ```""" voice_client = _get_client(bot, message.guild) response = 'No songs queued' if voice_client and hasattr(voice_client, 'song_queue') and voice_client.song_queue: song_list = [f' • {song.title}' for song in voice_client.song_queue] response = 'Songs currently in the queue:\n' response += '\n'.join(song_list) return Message(message=response)
async def summon(bot, message): """``` Bring Senpai to your current voice channel. Usage: * /summon ```""" if message.author.voice is None or message.author.voice.channel is None: return Message(message='You are not in a voice channel') summoned_voice_channel = message.author.voice.channel voice_client = _get_client(bot, message.guild) if voice_client is None: await summoned_voice_channel.connect() elif voice_client.channel != summoned_voice_channel: await voice_client.move_to(summoned_voice_channel)
async def cmd(bot, message): """``` Run an arbitrary command on the server Senpai runs on. Usage: * /cmd sudo yum -y update ```""" pieces = message.content.strip().split(' ') if len(pieces) < 2: return command = ' '.join(pieces[1:]) stdout, stderr = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() if stdout or stderr: return Message(f'```Output: {stdout}\n\nError: {stderr}```')
async def callme(bot, message): """``` In the future, Senpai will call you whatever name you provide. Use /hi to check your name. /callme name ```""" author_id = str(message.author.id) new_name = message.content[8:].strip() with open(NAMES_FILE, 'r') as f: names = json.load(f) names[author_id] = new_name with open(NAMES_FILE, 'w') as f: json.dump(names, f) return Message(message=f'Got it {new_name}!')
async def _play_mp3(bot, message, clip: str): # Requester must be in a voice channel to use '/<sound>' command if message.author.voice.channel is None: return Message(message='You are not in a voice channel.') voice_client = _get_client(bot, message.guild) if voice_client is None: response = await summon(bot, message) if response: return response else: voice_client = _get_client(bot, message.guild) source = discord.PCMVolumeTransformer( discord.FFmpegPCMAudio(f'sounds/{clip}.mp3')) # add information for now-playing command source.title = f'Sound clip - {clip}' source.url_link = None _play_or_queue(voice_client, source)
def handle_command_character_message(self, message, channel_id): msg = message['msg'].lstrip(self.command_character) command = msg.split()[0].lower() arguments = " ".join(msg.split()[1:]) user = message['u']['username'] attachments = message['attachments'] pass_message = Message(message_id=message["_id"], text=msg, chat=Chat(chat_id=channel_id), user=User.from_message(message), attachments=attachments, json=message) for cmd_list in self.commands: if command.lower() in cmd_list[0]: cmd_list[1](pass_message) return if not self.handle_auto_answer(message, self.direct_answers, channel_id): self.send_message('@' + user + ' :' + choice(self.unknown_command), channel_id)
async def remind(bot, message): """``` Ask Senpai to remind you, or a channel, about something at a given time. Times use 24-hour time and assume EST. Usages: * /remind me at 2019-05-01 08:00 this is my reminder * /remind me in 3 days this is my reminder * /remind here at 2019-05-01 14:00 this is my reminder * /remind here in 7 hours this is my reminder ```""" new_event = { 'name': f'remind_{str(uuid.uuid4())}', 'handler': 'reminder', 'frequency': 'once', # for flavor, has no real use 'channel_id': message.channel.id, 'author_id': message.author.id, 'cleanup': True, } pieces = message.content.split(' ') if len(pieces) < 6: return Message( 'Not enough arguments provided, see "/senpai remind" for example formats' ) # Figure out where to send the target = pieces[1] if target not in valid_targets: return Message( f'Invalid target "{target}", must be one of the following: {", ".join(valid_targets)}' ) new_event['target'] = target # Figure out what time to send the reminder keyword = pieces[2] if keyword == 'at': day_minute = ' '.join(pieces[3:5]) try: target_time = datetime.strptime(day_minute, Ids.ABSOLUTE_TIME_FORMAT) # Assume given time is in EST, convert to UTC est_now = datetime.now(pytz.timezone('US/Eastern')) offset = (-1) * est_now.utcoffset().total_seconds() / 3600 target_time += timedelta(hours=offset) except ValueError: return Message( 'Bad date format, expected something like "2019-01-01 01:05"') if target_time < datetime.utcnow(): return Message( f'Scheduled time for the event, {day_minute}, occurs in the past' ) elif keyword == 'in': count = pieces[3] try: n = int(count) except ValueError: return Message('Non-numeric value for "n"') if n <= 0: return Message('"n" must be greater than 0') period = pieces[4] valid_periods = ['months', 'days', 'hours', 'minutes'] valid_periods_singular = [ valid_period[:-1] for valid_period in valid_periods ] if period not in valid_periods + valid_periods_singular: return Message( f'Invalid period "{period}", must be one of the following: {", ".join(valid_periods)}' ) if period in valid_periods_singular: period = period + 's' kwargs = {period: n} target_time = datetime.utcnow() + timedelta(**kwargs) else: return Message( f'Invalid keyword "{keyword}", must be one of the following: at, in' ) new_event['absolute_start'] = datetime.strftime(target_time, Ids.ABSOLUTE_TIME_FORMAT) new_event['absolute_end'] = datetime.strftime( target_time + timedelta(minutes=5), Ids.ABSOLUTE_TIME_FORMAT) # Rest of the message is the reminder text to send reminder = ' '.join(pieces[5:]) new_event['reminder'] = reminder with Ids.FILE_LOCK: with open(Ids.EVENTS_FILE, 'r') as f: events = json.load(f) events.append(new_event) with open(Ids.EVENTS_FILE, 'w') as f: json.dump(events, f, indent=4) return Message('Got it.')
async def play_clip(bot, message): bot_message = await _play_mp3(bot, message, message.content[1:].strip()) return Message(message=bot_message) if bot_message else None
async def parse_roll(dice_string): if dice_string == 'stats': response = await stats() return Message(message=response) global totalDice totalDice = 0 dice_string = dice_string.replace(' ', '') try: dice_array = split_string(dice_string, '+') dice_array = split_array(dice_array, '-') dice_array = split_array(dice_array, '*') dice_array = split_array(dice_array, '/') dice_array = split_array(dice_array, '^') dice_array = split_array(dice_array, '(') dice_array = split_array(dice_array, ')') except Exception as e: LOG.error('Error splitting dice', exc_info=e) return Message(message='Ask Ian about whatever you just tried to do') # Roll for an NdM parts and substitute in the result in parenthesis for index, item in enumerate(dice_array): if 'd' in item: try: string_result = roll_dice(item) del dice_array[index] dice_array.insert(index, f'({string_result})') except RuntimeError as e: if 'Bad dice format' in str(e): LOG.debug('bad dice format') return Message( message='Use an NdM format for rolls please.') elif 'Numbers too big' in str(e): LOG.debug('numbers too big') return Message(message='You ask for too much.') else: LOG.error('Unknown dice error', exc_info=e) return Message( message='Ask Ian about whatever you just tried to do') result_string = ''.join(dice_array) # Attempt to evaluate the expression nsp = NumericStringParser() try: result = nsp.eval(result_string.replace(' ', '')) except ParseException: LOG.debug(f'could not evaluate {result_string}') return Message(message='Bad expression') except RecursionError: LOG.debug('recursion limit reached.') return Message( message='Too many operations are needed to evaluate that') except OverflowError: LOG.debug('parse_roll: overflow error') return Message(message='Overflow error, the result was too big') # Convert to integer, if possible if not isinstance(result, complex): if result == int(result): result = int(result) else: result = round(result, 4) result_string += f'\n\t = {result}' # Tell discord message to ignore * use for italics result_string.replace('*', '\*') # Discord puts a 2000 character limit on messages return Message(message=result_string, cleanup_original=False, cleanup_self=False)