Beispiel #1
0
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.')
Beispiel #2
0
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)
Beispiel #3
0
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)
Beispiel #4
0
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')
Beispiel #5
0
    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)
Beispiel #6
0
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
Beispiel #7
0
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
Beispiel #8
0
    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
Beispiel #9
0
    async def count(bot, message):
        """```
        Give the number of available sound clips.

        Usage:
        * /count
        ```"""
        return Message(message=str(len(clips)) + ' sound clips')
Beispiel #10
0
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)
Beispiel #11
0
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
Beispiel #12
0
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}')
Beispiel #13
0
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.')
Beispiel #14
0
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')
Beispiel #15
0
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')
Beispiel #16
0
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)
Beispiel #17
0
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))
Beispiel #18
0
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}')
Beispiel #19
0
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)
Beispiel #20
0
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)
Beispiel #21
0
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}```')
Beispiel #22
0
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}!')
Beispiel #23
0
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)
Beispiel #24
0
    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)
Beispiel #25
0
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.')
Beispiel #26
0
 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
Beispiel #27
0
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)