Beispiel #1
0
async def add_text(bot, context):
    text_type, new_text = context.arguments
    table_name = 'txyz_' + TYPE_NAMES[text_type]
    data.db_insert(bot,
                   table_name,
                   specifiers='value',
                   input_args=new_text,
                   safe=False)
    return Response(content='Added a {}.'.format(table_name[5:-1]))
Beispiel #2
0
def schedule(bot,
             plugin_name,
             time,
             function,
             payload=None,
             search=None,
             destination=None,
             info=None):
    """Adds the entry to the schedule table and starts the timer.

    It should be noted that the function CANNOT be a lambda function. It must
        be a function residing in the plugin.
    The payload should be a standard dictionary.
    The search keyword argument is to assist in later deletion or modification.
    Time should be a number in seconds from the epoch.
    """
    input_args = [
        int(time), plugin_name, function.__name__,
        json.dumps(payload) if payload else None, search, destination, info
    ]
    data.db_insert(bot, 'schedule', input_args=input_args, safe=False)
    asyncio.ensure_future(_start_scheduler(bot))
Beispiel #3
0
def schedule(
        bot, plugin_name, scheduled_time, function, payload=None,
        search=None, destination=None, info=None):
    """Adds the entry to the schedule table and starts the timer.

    It should be noted that the function CANNOT be a lambda function. It must
        be a function residing in the top level of the plugin.
    Time should be a number in seconds from the epoch.

    The asynchronous function should take 6 arguments:
    bot -- An instance of the bot.
    scheduled_time -- Time at which the given function should be called.
    payload -- Same as the keyword argument.
    search -- Same as the keyword argument.
    destination -- Same as the keyword argument.
    late -- Whether or not the function was called late due to bot downtime.
    info -- Same as the keyword argument.
    id -- Unique ID assigned to the entry when it was created. Usually unused.

    Keyword arguments:
    payload -- Standard json-serializable dictionary
    search -- Used to assist in later deletion or modification
    destination -- Starts with either a 'c' or 'u', then the ID of the channel or user
        This is used to help determine what will need to be messaged.
    info -- Used as a description for the scheduled event if `!base notifications` is used.
    """
    input_args = [
        int(scheduled_time),
        plugin_name,
        function.__name__,
        Json(payload),
        search,
        destination,
        info
    ]
    data.db_insert(bot, 'schedule', input_args=input_args, safe=False)
    asyncio.ensure_future(_start_scheduler(bot))
Beispiel #4
0
async def add_text(bot, context):
    text_type, new_text = context.arguments
    table_name = 'txyz_' + TYPE_NAMES[text_type]
    data.db_insert(bot, table_name, specifiers='value', input_args=new_text, safe=False)
    return Response(content='Added a {}.'.format(table_name[5:-1]))
Beispiel #5
0
async def _process_data(bot, author, url, propagate_error=False):
    """Checks the given data and edits/adds an entry to the database."""
    error_code = 1
    raw_data = await utilities.download_url(bot, url, use_fp=True)
    reader = codecs.getreader('utf-8')  # SO: 6862770
    try:
        parsed = json.load(reader(raw_data))
    except Exception as e:
        raise CBException("Failed to load the raw data.", e=e)

    # Check that values are within proper ranges
    try:
        if 'version' not in parsed:
            raise CBException("Missing version number.")
        if 'type' not in parsed:
            raise CBException("Missing character type.")
        if 'name' not in parsed:
            raise CBException("Missing name.")
        if 'attributes' not in parsed:
            raise CBException("Missing attributes.")
        if 'attribute_order' not in parsed:
            raise CBException("Missing attribute order.")
        if 'thumbnail' not in parsed:
            raise CBException("Missing thumbnail.")
        if 'images' not in parsed:
            raise CBException("Missing images.")
        if 'embed_color' not in parsed:
            raise CBException("Missing embed color.")
        if 'tags' not in parsed:
            raise CBException("Missing tags.")

        # Check version
        total_characters = 0
        version = parsed['version']
        if not isinstance(version, int):
            raise CBException("Invalid version type. [int]")
        if version != DATA_VERSION:
            error_code = 4
            raise CBException(
                "Invalid or outdated data format. Please use the character creator site.")

        # Check type and name
        character_type = parsed['type']
        if character_type not in CHARACTER_TYPES:
            raise CBException("Invalid character type.")
        name = parsed['name']
        clean_name = _clean_text_wrapper(name)
        if not isinstance(name, str):
            raise CBException("Invalid name type. [string]")
        if not 1 <= len(name) <= 100:
            raise CBException("Invalid name length. [1-100]")
        total_characters += len(name)

        # Check attributes
        attributes = parsed['attributes']
        if not isinstance(attributes, dict):
            raise CBException("Invalid attributes type. [dictionary]")
        if not 0 <= len(attributes) <= 20:
            raise CBException("Invalid number of attributes. [1-20]")
        for key, value in attributes.items():
            if not isinstance(value, str):
                raise CBException("An attribute has an invalid type. [string]")
            if not 1 <= len(key) <= 50:
                raise CBException("Invalid attribute name length. [1-50]")
            if key in COMMON_ATTRIBUTES and not 1 <= len(value) <= 50:
                raise CBException("Invalid common attribute value length. [1-50]")
            elif not 1 <= len(value) <= 1000:
                raise CBException("Invalid attribute value length. [1-1000]")
            total_characters += len(key) + len(value)

        # Check thumbnail
        thumbnail = parsed['thumbnail']
        if not isinstance(thumbnail, (str, type(None))):
            raise CBException("Invalid thumbnail type. [string]")
        if isinstance(thumbnail, str) and not _valid_url(thumbnail):
            error_code = 2
            raise CBException("Invalid thumbnail URL.")

        # Check images
        images = parsed['images']
        if not isinstance(images, list):
            raise CBException("Invalid images type. [list]")
        if not 0 <= len(images) <= 10:
            raise CBException("Invalid number of images. [0-10]")
        for image in images:
            if not isinstance(image, list):
                raise CBException("Invalid image type. [list]")
            if len(image) != 3:
                raise CBException("Invalid image metadata length. [3]")
            for meta in image:
                if not isinstance(meta, str):
                    raise CBException("Invalid image metadata type. [string]")
            if not 1 <= len(image[0]) <= 500:  # Direct image URL
                raise CBException("Invalid direct image URL length. [1-500]")
            if not _valid_url(image[0]):
                error_code = 3
                raise CBException("Invalid direct image URL.")
            if not 0 <= len(image[1]) <= 500:  # Source URL
                raise CBException("Invalid source URL length. [0-500]")
            if not 0 <= len(image[2]) <= 100:  # Caption
                raise CBException("Invalid image caption length. [0-100]")

        # Check embed color
        embed_color = parsed['embed_color']
        if not isinstance(embed_color, (int, type(None))):
            raise CBException("Invalid embed color type. [int]")
        if isinstance(embed_color, int) and not 0x0 <= embed_color <= 0xffffff:
            raise CBException("Invalid embed color range. [0x0-0xffffff]")

        # Version 2 stuff
        attribute_order = parsed['attribute_order']
        if not isinstance(attribute_order, list):
            raise CBException("Invalid attribute order type. [list]")
        tags = parsed['tags']
        if not isinstance(tags, list):
            raise CBException("Invalid tags type. [list]")

        # Check attribute_order
        order_set = set(attribute_order)
        attribute_set = set(attributes)
        if len(attribute_order) != len(order_set):
            raise CBException("Duplicate attribute order entry.")
        if order_set != attribute_set:
            raise CBException("Attribute order does not match attribute set.")

        # Check tags
        tags = parsed['tags']
        tags_raw = parsed['tags_raw']
        if not 0 <= len('#'.join(tags)) <= 260:  # +60 for name and type
            raise CBException("Invalid tags length. [0-200]")
        if clean_name not in tags:
            raise CBException("Character name not in tags.")
        for character_type in CHARACTER_TYPES:
            if character_type in tags:
                break
        else:
            raise CBException("Character type not in tags.")
        if len(set(tags)) != len(tags):
            raise CBException("Duplicate tags exist.")
        for tag in tags:
            test = _clean_text_wrapper(tag, lowercase=False)
            if test != tag:
                raise CBException("Invalid tag.")
            total_characters += len(tag)

        if total_characters > 3000:
            raise CBException("Total characters exceeded 3000.")

    except BotException as e:
        if propagate_error:
            raise e
        else:
            await author.send("The data checks failed. Error:\n{}".format(e.error_details))
            return error_code

    created_time = int(time.time())
    dt = datetime.datetime.utcfromtimestamp(created_time)

    json_data = Json({
        'type': character_type,
        'version': DATA_VERSION,
        'name': name,
        'clean_name': clean_name,
        'owner_id': author.id,
        'attributes': attributes,
        'attribute_order': attribute_order,
        'thumbnail': thumbnail,
        'images': images,
        'embed_color': embed_color,
        'tags': tags,
        'tags_raw': tags_raw,
        'created': created_time
    })

    # Check for edit or entry creation
    cursor = data.db_select(
        bot, select_arg='clean_name', from_arg='characters', where_arg='owner_id=%s',
        input_args=[author.id])
    existing_names = [it[0] for it in cursor.fetchall()] if cursor else []
    if clean_name in existing_names:  # Edit
        data.db_update(
            bot, 'characters', set_arg='name=%s, data=%s, tags=%s, modified=%s',
            where_arg='owner_id=%s AND clean_name=%s',
            input_args=(name, json_data, tags, dt, author.id, clean_name))
        content = "Edited the entry for {}.".format(name)
    else:  # Create
        data.db_insert(
            bot, 'characters', input_args=[author.id, name, clean_name, json_data, tags, dt])
        content = "Created a new entry for {}.".format(name)

    if propagate_error:
        return content
    else:
        await author.send(content)
        return 0
Beispiel #6
0
async def _violation_notification(bot, message, awoo_tier, send_message=True):
    """
    Logs the violation and (optionally) sends the user a notification.

    Standard notification: once per violation, up to 1 time
    None: 2 violations
    Silence notification: 1 violation

    Reset period for notifications is 1 minute.

    Stress indicates a number of users making a violation within a 60 second period.
    Tier 1: 3 members
    Tier 2: 5 members
    Tier 3: 8 members
    """

    author, channel = message.author, message.channel
    current_time = time.time()
    violation_data = data.get(
        bot, __name__, 'user_violation', user_id=author.id, volatile=True)
    channel_violation_data = data.get(
        bot, __name__, 'channel_violation', channel_id=channel.id, volatile=True)
    if not violation_data or current_time - violation_data['time'] >= 60:
        violation_data = {'time': 0, 'violations': 0}
        data.add(bot, __name__, 'user_violation', violation_data, user_id=author.id, volatile=True)
    if not channel_violation_data or current_time - channel_violation_data['time'] >= 60:
        channel_violation_data = {'time': 0, 'violators': set(), 'sent_tier': 0}
        data.add(
            bot, __name__, 'channel_violation', channel_violation_data,
            channel_id=channel.id, volatile=True)
    violation_data['violations'] += 1
    violation_data['time'] = current_time
    channel_violation_data['violators'].add(author.id)
    channel_violation_data['time'] = current_time

    # Update table
    set_arg = 'debt = debt+%s, violations = violations+1'
    if awoo_tier == 2:
        set_arg += ', sneaky = sneaky+1'
    cursor = data.db_select(bot, from_arg='awoo', where_arg='user_id=%s', input_args=[author.id])
    entry = cursor.fetchone() if cursor else None
    if entry:
        data.db_update(
            bot, 'awoo', set_arg=set_arg, where_arg='user_id=%s', input_args=[fine, author.id])
    else:
        data.db_insert(bot, 'awoo', input_args=[author.id, fine, 1, 1 if awoo_tier == 2 else 0])

    # Add a snarky message depending on the tier
    if awoo_tier == 2:  # Attempted bypass
        snark = random.choice(statements['bypass']) + '\n'
    elif awoo_tier == 3:  # Legalization plea
        snark = random.choice(statements['legalize']) + '\n'
    else:
        snark = ''

    # Notify user
    logger.debug("Violations: %s", violation_data['violations'])
    text = ''
    if violation_data['violations'] <= 1:
        text = "{}{} has been fined ${} for an awoo violation.".format(snark, author.mention, fine)
    elif violation_data['violations'] == 4:
        text = "{} {}".format(author.mention, random.choice(statements['silence']))
    elif awoo_tier == 3 and violation_data['violations'] <= 3:  # Legalization plea, but silent
        text = snark
    if send_message and text:
        await channel.send(content=text)
    else:
        await message.add_reaction(random.choice(['🚩', '🛑', '�', '⛔', '🚫']))

    # Stress
    violators, sent_tier = channel_violation_data['violators'], channel_violation_data['sent_tier']
    if (len(violators) == 3 and sent_tier == 0 or
            len(violators) == 5 and sent_tier == 1 or
            len(violators) == 8 and sent_tier == 2):
        if send_message:
            await message.channel.send(random.choice(statements['stress'][sent_tier]))
        channel_violation_data['sent_tier'] += 1
Beispiel #7
0
async def _violation_notification(bot, message, awoo_tier, send_message=True):
    """
    Logs the violation and (optionally) sends the user a notification.

    Standard notification: once per violation, up to 1 time
    None: 2 violations
    Silence notification: 1 violation

    Reset period for notifications is 1 minute.

    Stress indicates a number of users making a violation within a 60 second period.
    Tier 1: 3 members
    Tier 2: 5 members
    Tier 3: 8 members
    """

    author, channel = message.author, message.channel
    current_time = time.time()
    violation_data = data.get(bot,
                              __name__,
                              'user_violation',
                              user_id=author.id,
                              volatile=True)
    channel_violation_data = data.get(bot,
                                      __name__,
                                      'channel_violation',
                                      channel_id=channel.id,
                                      volatile=True)
    if not violation_data or current_time - violation_data['time'] >= 60:
        violation_data = {'time': 0, 'violations': 0}
        data.add(bot,
                 __name__,
                 'user_violation',
                 violation_data,
                 user_id=author.id,
                 volatile=True)
    if not channel_violation_data or current_time - channel_violation_data[
            'time'] >= 60:
        channel_violation_data = {
            'time': 0,
            'violators': set(),
            'sent_tier': 0
        }
        data.add(bot,
                 __name__,
                 'channel_violation',
                 channel_violation_data,
                 channel_id=channel.id,
                 volatile=True)
    violation_data['violations'] += 1
    violation_data['time'] = current_time
    channel_violation_data['violators'].add(author.id)
    channel_violation_data['time'] = current_time

    # Update table
    set_arg = 'debt = debt+%s, violations = violations+1'
    if awoo_tier == 2:
        set_arg += ', sneaky = sneaky+1'
    cursor = data.db_select(bot,
                            from_arg='awoo',
                            where_arg='user_id=%s',
                            input_args=[author.id])
    entry = cursor.fetchone() if cursor else None
    if entry:
        data.db_update(bot,
                       'awoo',
                       set_arg=set_arg,
                       where_arg='user_id=%s',
                       input_args=[fine, author.id])
    else:
        data.db_insert(
            bot,
            'awoo',
            input_args=[author.id, fine, 1, 1 if awoo_tier == 2 else 0])

    # Add a snarky message depending on the tier
    if awoo_tier == 2:  # Attempted bypass
        snark = random.choice(statements['bypass']) + '\n'
    elif awoo_tier == 3:  # Legalization plea
        snark = random.choice(statements['legalize']) + '\n'
    else:
        snark = ''

    # Notify user
    logger.debug("Violations: %s", violation_data['violations'])
    text = ''
    if violation_data['violations'] <= 1:
        text = "{}{} has been fined ${} for an awoo violation.".format(
            snark, author.mention, fine)
    elif violation_data['violations'] == 4:
        text = "{} {}".format(author.mention,
                              random.choice(statements['silence']))
    elif awoo_tier == 3 and violation_data[
            'violations'] <= 3:  # Legalization plea, but silent
        text = snark
    if send_message and text:
        await channel.send(content=text)
    else:
        await message.add_reaction(
            random.choice(['🚩', '🛑', '�', '⛔', '🚫']))

    # Stress
    violators, sent_tier = channel_violation_data[
        'violators'], channel_violation_data['sent_tier']
    if (len(violators) == 3 and sent_tier == 0
            or len(violators) == 5 and sent_tier == 1
            or len(violators) == 8 and sent_tier == 2):
        if send_message:
            await message.channel.send(
                random.choice(statements['stress'][sent_tier]))
        channel_violation_data['sent_tier'] += 1
Beispiel #8
0
async def _dump(bot,
                dump_data,
                log_channel,
                details='No details provided.',
                query=None,
                moderator_id=None,
                logged_channels=[]):
    """Dumps the given built dump data to the log channel.
    
    logged_channels specifies what channels to log. If no channels are given, this logs
        all channels by default.
    """
    built_query = '&highlight={}'.format(query) if query else ''
    logged_channel_ids = [it.id for it in logged_channels]
    guild = log_channel.guild

    # Remove extra channels and members
    if logged_channels:
        valid_members = set()
        to_remove = []
        total_messages = 0
        for channel_id, channel_data in dump_data['channels'].items():
            if int(channel_id) in logged_channel_ids:
                for message in channel_data['messages']:
                    valid_members.add(message['author'])
                total_messages += len(channel_data['messages'])
            else:
                to_remove.append(channel_id)
        for it in to_remove:
            del dump_data['channels'][it]
        to_remove = [
            it for it in dump_data['members'] if it not in valid_members
        ]
        for it in to_remove:
            del dump_data['members'][it]
    else:
        total_messages = dump_data['total']

    # Build full dump string
    full_dump = json.dumps(dump_data)

    # Send logs and get session code
    log_message = await utilities.send_text_as_file(log_channel, full_dump,
                                                    'logs')
    url = log_message.attachments[0].url
    session_code = '{}:{}'.format(
        *[it[::-1] for it in url[::-1].split('/')[2:0:-1]])

    # Build embed data
    embed = discord.Embed(
        title='Click here to view the message logs',
        url='https://jkchen2.github.io/log-viewer?session={}{}'.format(
            session_code, built_query),
        timestamp=datetime.utcnow())
    embed.add_field(name='Details', value=details, inline=False)
    if logged_channels:
        embed.add_field(name='Channels',
                        value=', '.join(it.mention for it in logged_channels))

    # Add incident number
    entry_data = [
        session_code,
        details,
        query,
        moderator_id,
        None,  # messageid
        int(time.time()),
        Json({})
    ]
    cursor = data.db_insert(bot,
                            'autolog',
                            table_suffix=guild.id,
                            input_args=entry_data,
                            create='autolog_template')
    inserted = cursor.fetchone()
    embed.set_footer(text='Incident #{}'.format(inserted.id))

    # Send embed and update messageid
    message = await log_channel.send(embed=embed)
    data.db_update(bot,
                   'autolog',
                   table_suffix=guild.id,
                   set_arg='messageid=%s',
                   where_arg='id=%s',
                   input_args=[message.id, inserted.id])
    return total_messages
Beispiel #9
0
async def _dump(
        bot, dump_data, log_channel, details='No details provided.',
        query=None, moderator_id=None, logged_channels=[]):
    """Dumps the given built dump data to the log channel.
    
    logged_channels specifies what channels to log. If no channels are given, this logs
        all channels by default.
    """
    built_query = '&highlight={}'.format(query) if query else ''
    logged_channel_ids = [it.id for it in logged_channels]
    guild = log_channel.guild

    # Remove extra channels and members
    if logged_channels:
        valid_members = set()
        to_remove = []
        total_messages = 0
        for channel_id, channel_data in dump_data['channels'].items():
            if int(channel_id) in logged_channel_ids:
                for message in channel_data['messages']:
                    valid_members.add(message['author'])
                total_messages += len(channel_data['messages'])
            else:
                to_remove.append(channel_id)
        for it in to_remove:
            del dump_data['channels'][it]
        to_remove = [it for it in dump_data['members'] if it not in valid_members]
        for it in to_remove:
            del dump_data['members'][it]
    else:
        total_messages = dump_data['total']

    # Build full dump string
    full_dump = json.dumps(dump_data)

    # Send logs and get session code
    log_message = await utilities.send_text_as_file(log_channel, full_dump, 'logs')
    url = log_message.attachments[0].url
    session_code = '{}:{}'.format(*[it[::-1] for it in url[::-1].split('/')[2:0:-1]])

    # Build embed data
    embed = discord.Embed(
        title='Click here to view the message logs',
        url='https://jkchen2.github.io/log-viewer?session={}{}'.format(session_code, built_query),
        timestamp=datetime.utcnow())
    embed.add_field(name='Details', value=details, inline=False)
    if logged_channels:
        embed.add_field(name='Channels', value=', '.join(it.mention for it in logged_channels))

    # Add incident number
    entry_data = [
        session_code,
        details,
        query,
        moderator_id,
        None,  # messageid
        int(time.time()),
        Json({})
    ]
    cursor = data.db_insert(
        bot, 'autolog', table_suffix=guild.id, input_args=entry_data, create='autolog_template')
    inserted = cursor.fetchone()
    embed.set_footer(text='Incident #{}'.format(inserted.id))

    # Send embed and update messageid
    message = await log_channel.send(embed=embed)
    data.db_update(
        bot, 'autolog', table_suffix=guild.id, set_arg='messageid=%s',
        where_arg='id=%s', input_args=[message.id, inserted.id])
    return total_messages