Example #1
0
def setup_characters_table(bot):
    """Creates the characters table and updates outdated entries."""
    data.db_create_table(bot, 'characters', template='characters_template')
    cursor = data.db_execute(
        bot, 'SELECT column_name FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = %s',
        input_args=['characters'])
    columns = [it[0] for it in cursor.fetchall()]
    if 'tags' not in columns:
        data.db_execute(bot, 'ALTER TABLE characters ADD COLUMN tags text[]')
    if 'modified' not in columns:
        data.db_execute(bot, 'ALTER TABLE characters ADD COLUMN modified timestamp')

    name_index = 'IX_character_order'
    if not data.db_exists(bot, name_index):
        data.db_execute(bot, 'CREATE INDEX {} ON characters (clean_name ASC)'.format(name_index))

    # Select all entries and convert
    cursor = data.db_select(bot, from_arg='characters')
    for entry in cursor.fetchall():
        if entry.data['version'] == 1:  # NOTE: Change per version bump
            dt = datetime.datetime.utcfromtimestamp(entry.data['created'])
            new_data = entry.data
            tags = [new_data['type'], entry.clean_name]
            new_data['attribute_order'] = list(new_data['attributes'].keys())
            new_data['images'] = [[it, '', ''] for it in new_data['images']]
            new_data['tags'] = tags
            new_data['version'] = DATA_VERSION
            data.db_update(
                bot, 'characters', set_arg='data=%s, tags=%s, modified=%s',
                where_arg='clean_name=%s AND owner_id=%s',
                input_args=(Json(new_data), tags, dt, entry.clean_name, entry.owner_id))
Example #2
0
def update_schedule_entries(
        bot, plugin_name, search=None, destination=None, function=None,
        payload=None, new_search=None, new_time=None, new_destination=None,
        info=None, custom_match=None, custom_args=[]):
    """Updates the schedule entry with the given fields.

    If any field is left as None, it will not be changed.
    If custom_match is given, it must be a proper WHERE SQL clause. Otherwise
        it will look for a direct match with search.

    Returns the number of entries modified.
    """
    if custom_match:
        where_arg = custom_match
        input_args = custom_args
    else:
        where_arg = 'plugin = %s'
        input_args = [plugin_name]
        if search is not None:
            where_arg += ' AND search = %s'
            input_args.append(search)
        if destination is not None:
            where_arg += ' AND destination = %s'
            input_args.append(destination)

    set_args = []
    set_input_args = []
    if function:
        set_args.append('function=%s')
        set_input_args.append(function.__name__)
    if payload:
        set_args.append('payload=%s')
        set_input_args.append(Json(payload))
    if new_time is not None:
        set_args.append('time=%s')
        set_input_args.append(int(new_time))
    if new_search is not None:
        set_args.append('search=%s')
        set_input_args.append(new_search)
    if new_destination:
        set_args.append('destination=%s')
        set_input_args.append(new_destination)
    if info is not None:
        set_args.append('info=%s')
        set_input_args.append(info)
    set_arg = ', '.join(set_args)
    input_args = set_input_args + input_args
    data.db_update(bot, 'schedule', set_arg=set_arg, where_arg=where_arg, input_args=input_args)
    asyncio.ensure_future(_start_scheduler(bot))
Example #3
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
Example #4
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
Example #5
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
Example #6
0
def update_schedule_entries(bot,
                            plugin_name,
                            search=None,
                            destination=None,
                            function=None,
                            payload=None,
                            new_search=None,
                            time=None,
                            new_destination=None,
                            info=None,
                            custom_match=None,
                            custom_args=[]):
    """Updates the schedule entry with the given fields.

    If any field is left as None, it will not be changed.
    If custom_match is given, it must be a proper WHERE SQL clause. Otherwise
        it will look for a direct match with search.

    Returns the number of entries modified.
    """
    if custom_match:
        where_arg = custom_match
        input_args = custom_args
    else:
        where_arg = 'plugin = %s'
        input_args = [plugin_name]
        if search is not None:
            where_arg += ' AND search = %s'
            input_args.append(search)
        if destination is not None:
            where_arg += ' AND destination = %s'
            input_args.append(destination)

    set_args = []
    set_input_args = []
    if function:
        set_args.append('function=%s')
        set_input_args.append(function.__name__)
    if payload:
        set_args.append('payload=%s')
        set_input_args.append(json.dumps(payload))
    if time:
        set_args.append('time=%s')
        set_input_args.append(int(time))
    if new_search:
        set_args.append('search=%s')
        set_input_args.append(new_search)
    if new_destination:
        set_args.append('destination=%s')
        set_input_args.append(new_destination)
    if info:
        set_args.append('info=%s')
        set_input_args.append(info)
    set_arg = ', '.join(set_args)
    input_args = set_input_args + input_args
    data.db_update(bot,
                   'schedule',
                   set_arg=set_arg,
                   where_arg=where_arg,
                   input_args=input_args)
    asyncio.ensure_future(_start_scheduler(bot))
Example #7
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
Example #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