Ejemplo n.º 1
0
    async def predicate(ctx: Context):
        # Get the roles of the user in the UWCS discord
        compsoc_guild = [
            guild for guild in ctx.bot.guilds
            if guild.id == CONFIG["UWCS_DISCORD_ID"]
        ][0]
        compsoc_member = compsoc_guild.get_member(ctx.message.author.id)
        if not compsoc_member:
            raise AdminError(
                f"You aren't part of the UWCS discord so I'm afraid I can't let you do that. :octagonal_sign:"
            )

        roles = list(
            map(
                lambda x: discord.utils.get(compsoc_member.roles, id=x),
                CONFIG["UWCS_EXEC_ROLE_IDS"],
            ))
        if not roles:
            if not isinstance(ctx.channel, PrivateChannel):
                await ctx.message.delete()
            display_name = get_name_string(ctx.message)
            raise AdminError(
                f"You don't have permission to run that command, {display_name}."
            )
        else:
            return True
Ejemplo n.º 2
0
    async def tex(self, ctx: Context, *message: clean_content):
        await ctx.trigger_typing()

        # Input filtering
        if not message:
            await ctx.send("Your message contained nothing to render")

        combined = " ".join([x.lstrip("@") for x in message])
        if combined[0] != "`" or combined[-1] != "`":
            await ctx.send("Please place your input in an inline code block")

        # Matplotlib preamble
        plt.clf()
        plt.rc("text", usetex=True)
        plt.rc("font", **{
            "family": "serif",
            "serif": ["Palatino"],
            "size": 16
        })
        plt.axis("off")

        # Generate the filename
        filename = (combined.lstrip("`").rstrip("`") + "-" +
                    str(hex(int(datetime.utcnow().timestamp()))).lstrip("0x") +
                    ".png").replace(" ", "")
        path_png = "{path}/{filename}".format(
            path=CONFIG["FIG_SAVE_PATH"].rstrip("/"), filename=filename)
        path_jpg = path_png.replace(".png", ".jpg")

        # Plot the latex and save it.
        plt.text(0, 1, combined.lstrip("`").rstrip("`"), color="white")
        plt.savefig(path_png, dpi=300, bbox_inches="tight", transparent=True)

        # Generate a mask of the transparent regions in the image
        img_arr = img_as_float(io.imread(path_png))
        transparent_mask = np.array([1, 1, 1, 0])
        img_mask = np.abs(img_arr - transparent_mask).sum(axis=2) < 1

        # Generate the bounding box for the mask
        mask_coords = np.array(np.nonzero(~img_mask))
        top_left = np.min(mask_coords, axis=1) - [15, 15]
        bottom_right = np.max(mask_coords, axis=1) + [15, 15]

        # Crop the image and add a background layer
        img_cropped = img_arr[top_left[0]:bottom_right[0],
                              top_left[1]:bottom_right[1]]
        img_cropped = color.rgba2rgb(img_cropped, background=IMAGE_BACKGROUND)

        # Save the image, delete the PNG and set the permissions for the JPEG
        io.imsave(path_jpg, img_cropped, quality=100)
        os.chmod(path_jpg, 0o644)
        os.remove(path_png)

        # Load the image as a file to be attached to an image
        img_file = File(path_jpg, filename="tex_output.jpg")
        display_name = get_name_string(ctx.message)
        await ctx.send(f"Here you go, {display_name}! :abacus:", file=img_file)
Ejemplo n.º 3
0
 async def predicate(ctx: Context):
     if not isinstance(ctx.channel, PrivateChannel):
         display_name = get_name_string(ctx.message)
         await ctx.message.delete()
         raise VerifyError(
             message=
             f"That command is supposed to be sent to me in a private message, {display_name}."
         )
     else:
         return True
Ejemplo n.º 4
0
    async def flip(self, ctx: Context, *args: clean_content):
        display_name = get_name_string(ctx.message)
        if len(args) == 1:
            await ctx.send(
                f"I can't flip just one item {display_name}! :confused:")
        else:
            options = ["Heads", "Tails"] if not args else args

            await ctx.send(
                f'{display_name}: {random.choice(options).lstrip("@")}')
Ejemplo n.º 5
0
 async def predicate(ctx: Context):
     roles = discord.utils.get(
         ctx.message.author.roles, id=CONFIG["UWCS_EXEC_ROLE_ID"]
     )
     if roles is None:
         await ctx.message.delete()
         display_name = get_name_string(ctx.message)
         raise BlacklistError(
             f"You don't have permission to run that command, {display_name}."
         )
     else:
         return True
Ejemplo n.º 6
0
    async def add(self, ctx: Context, *args: clean_content):
        if not args:
            await ctx.send("You're missing a time and a message!")
        else:
            trigger_time = parse_time(args[0])
            now = datetime.now()
            if not trigger_time:
                await ctx.send("Incorrect time format, please see help text.")
            elif trigger_time < now:
                await ctx.send("That time is in the past.")
            else:
                # HURRAY the time is valid and not in the past, add the reminder
                display_name = get_name_string(ctx.message)

                # set the id to a random value if the author was the bridge bot, since we wont be using it anyways
                # if ctx.message.clean_content.startswith("**<"): <---- FOR TESTING
                if ctx.message.author.id == CONFIG[
                        "UWCS_DISCORD_BRIDGE_BOT_ID"]:
                    author_id = 1
                    irc_n = display_name
                else:
                    author_id = (db_session.query(User).filter(
                        User.user_uid == ctx.message.author.id).first().id)
                    irc_n = None

                if len(args) > 1:
                    rem_content = " ".join(args[1:])
                    trig_at = trigger_time
                    trig = False
                    playback_ch_id = ctx.message.channel.id
                    new_reminder = Reminder(
                        user_id=author_id,
                        reminder_content=rem_content,
                        trigger_at=trig_at,
                        triggered=trig,
                        playback_channel_id=playback_ch_id,
                        irc_name=irc_n,
                    )
                    db_session.add(new_reminder)
                    try:
                        db_session.commit()
                        await ctx.send(
                            f'Thanks {display_name}, I have saved your reminder (but please note that my granularity is set at {CONFIG["REMINDER_SEARCH_INTERVAL"]} seconds).'
                        )
                    except:
                        db_session.rollback()
                        await ctx.send(f"Something went wrong")
                else:
                    await ctx.send("Please include some reminder text!")
Ejemplo n.º 7
0
    async def info(self, ctx: Context, filament_name: clean_content):
        filament = (db_session.query(FilamentType).filter(
            FilamentType.name.like(filament_name)).first())

        if not filament:
            await ctx.send(
                f'Couldn\'t find a filament that matches the name "{filament_name}"'
            )
            return

        # Construct the embed
        embed_colour = Color.from_rgb(61, 83, 255)
        embed_title = f'Filament info for "{filament_name}"'
        host = CONFIG["FIG_HOST_URL"] + "/filaments"
        image_file = filament.image_path.split("/")[-1]

        embed = Embed(title=embed_title, color=embed_colour)
        embed.add_field(name="Cost per kilogram",
                        value="{0:.2f}".format(filament.cost))
        embed.set_image(url=f"{host}/{image_file}")

        display_name = get_name_string(ctx.message)
        await ctx.send(f"Here you go, {display_name}! :page_facing_up:",
                       embed=embed)
Ejemplo n.º 8
0
def process_karma(message: Message, message_id: int, db_session: Session,
                  timeout: int):
    reply = ""

    # Parse the message for karma modifications
    raw_karma = parse_message(message.clean_content, db_session)

    # If no karma'd items, just return
    if not raw_karma:
        return reply

    # TODO: Protect from byte-limit length chars

    # If the author was IRC, set the display name to be the irc user that karma'd, else use original display name
    display_name = message.author.display_name
    if message.author.id == CONFIG["UWCS_DISCORD_BRIDGE_BOT_ID"]:
        # Gets the username of the irc user
        display_name = message.content.split(" ")[0][3:-3]

    # Process the raw karma tokens into a number of karma transactions
    transactions = create_transactions(message.author.name, display_name,
                                       raw_karma)

    if not transactions:
        return reply

    # Get karma-ing user
    user = db_session.query(User).filter(
        User.user_uid == message.author.id).first()

    # Start preparing the reply string
    if len(transactions) > 1:
        transaction_plural = "s"
    else:
        transaction_plural = ""

    item_str = ""
    error_str = ""

    # Iterate over the transactions to write them to the database
    for transaction in transactions:
        # Truncate the name safely so we 2000 char karmas can be used
        truncated_name = ((transaction.name[300:] +
                           ".. (truncated to 300 chars)")
                          if len(transaction.name) > 300 else transaction.name)

        # Catch any self-karma transactions early
        if transaction.self_karma and transaction.net_karma > -1:
            error_str += f' • Could not change "{truncated_name}" because you cannot change your own karma! :angry:\n'
            continue

        # Get the karma item from the database if it exists
        karma_item = (db_session.query(Karma).filter(
            func.lower(Karma.name) == func.lower(
                transaction.name)).one_or_none())

        # Update or create the karma item
        if not karma_item:
            karma_item = Karma(name=transaction.name)
            db_session.add(karma_item)
            db_session.commit()

        # Get the last change (or none if there was none)
        last_change = (db_session.query(KarmaChange).filter(
            KarmaChange.karma_id == karma_item.id).order_by(
                desc(KarmaChange.created_at)).first())

        if not last_change:
            # If the bot is being downvoted then the karma can only go up
            if transaction.name.lower() == "apollo":
                new_score = abs(transaction.net_karma)
            else:
                new_score = transaction.net_karma

            karma_change = KarmaChange(
                karma_id=karma_item.id,
                user_id=user.id,
                message_id=message_id,
                reasons=transaction.reasons,
                change=new_score,
                score=new_score,
                created_at=datetime.utcnow(),
            )
            db_session.add(karma_change)
            db_session.commit()
        else:
            time_delta = datetime.utcnow() - last_change.created_at

            if time_delta.seconds >= timeout:
                # If the bot is being downvoted then the karma can only go up
                if transaction.name.lower() == "apollo":
                    new_score = last_change.score + abs(transaction.net_karma)
                else:
                    new_score = last_change.score + transaction.net_karma

                karma_change = KarmaChange(
                    karma_id=karma_item.id,
                    user_id=user.id,
                    message_id=message_id,
                    reasons=transaction.reasons,
                    score=new_score,
                    change=(new_score - last_change.score),
                    created_at=datetime.utcnow(),
                )
                db_session.add(karma_change)
                db_session.commit()
            else:
                # Tell the user that the item is on cooldown
                if time_delta.seconds < 60:
                    error_str += f' • Could not change "{truncated_name}" since it is still on cooldown (last altered {time_delta.seconds} seconds ago).\n'
                else:
                    mins_plural = ""
                    mins = floor(time_delta.seconds / 60)
                    if time_delta.seconds >= 120:
                        mins_plural = "s"
                    error_str += f' • Could not change "{truncated_name}" since it is still on cooldown (last altered {mins} minute{mins_plural} ago).\n'

                continue

        # Update karma counts
        if transaction.net_karma == 0:
            karma_item.neutrals = karma_item.neutrals + 1
        elif transaction.net_karma == 1:
            karma_item.pluses = karma_item.pluses + 1
        elif transaction.net_karma == -1:
            # Make sure the changed operation is updated
            if transaction.name.lower() == "apollo":
                karma_item.pluses = karma_item.pluses + 1
            else:
                karma_item.minuses = karma_item.minuses + 1

        # Give some sass if someone is trying to downvote the bot
        if (transaction.name.casefold() == "Apollo".casefold()
                and transaction.net_karma < 0):
            apollo_response = ":wink:"
        else:
            apollo_response = ""

        # Build the karma item string
        if transaction.reasons:
            if len(transaction.reasons) > 1:
                reasons_plural = "s"
                reasons_has = "have"
            else:
                reasons_plural = ""
                reasons_has = "has"

            if transaction.self_karma:
                item_str += f" • **{truncated_name}** (new score is {karma_change.score}) and your reason{reasons_plural} {reasons_has} been recorded. *Fool!* that's less karma to you. :smiling_imp:\n"
            else:
                item_str += f" • **{truncated_name}** (new score is {karma_change.score}) and your reason{reasons_plural} {reasons_has} been recorded. {apollo_response}\n"
        else:
            if transaction.self_karma:
                item_str += f" • **{truncated_name}** (new score is {karma_change.score}). *Fool!* that's less karma to you. :smiling_imp:\n"
            else:
                item_str += f" • **{truncated_name}** (new score is {karma_change.score}). {apollo_response}\n"

    # Get the name, either from discord or irc
    author_display = get_name_string(message)

    # Construct the reply string in totality
    # If you have error(s) and no items processed successfully
    if not item_str and error_str:
        reply = f"Sorry {author_display}, I couldn't karma the requested item{transaction_plural} because of the following problem{transaction_plural}:\n\n{error_str}"
    # If you have items processed successfully but some errors too
    elif item_str and error_str:
        reply = f"Thanks {author_display}, I have made changes to the following item(s) karma:\n\n{item_str}\n\nThere were some issues with the following item(s), too:\n\n{error_str}"
    # If all items were processed successfully
    else:
        reply = f"Thanks {author_display}, I have made changes to the following karma item{transaction_plural}:\n\n{item_str}"

    # Commit any changes (in case of any DB inconsistencies)
    db_session.commit()
    return reply.rstrip()
Ejemplo n.º 9
0
 async def fact(self, ctx: Context):
     display_name = get_name_string(ctx.message)
     await ctx.send(f"{display_name}: {random.choice(self.options)}")
Ejemplo n.º 10
0
    async def plot(self, ctx: Context, *args: clean_content):
        await ctx.trigger_typing()
        t_start = current_milli_time()

        # If there are no arguments
        if not args:
            raise KarmaError(message="I can't")

        karma_dict = dict()
        failed = []

        # Iterate over the karma item(s)
        for karma in args:
            karma_stripped = karma.lstrip("@")
            karma_item = (db_session.query(KarmaModel).filter(
                func.lower(KarmaModel.name) == func.lower(
                    karma_stripped)).first())

            # Bucket the karma item(s) based on existence in the database
            if not karma_item:
                failed.append((karma_stripped, "hasn't been karma'd"))
                continue

            # Check if the topic has been karma'd >=10 times
            if len(karma_item.changes) < 5:
                failed.append((
                    karma_stripped,
                    f"must have been karma'd at least 5 times before a plot can be made (currently karma'd {len(karma_item.changes)} {pluralise(karma_item.changes, 'time')})",
                ))
                continue

            # Add the karma changes to the dict
            karma_dict[karma_stripped] = karma_item.changes

        # Plot the graph and save it to a png
        filename, path = await plot_karma(karma_dict)
        t_end = current_milli_time()

        if CONFIG["DEBUG"]:
            # Attach the file as an image for dev purposes
            plot_image = open(path, mode="rb")
            plot = File(plot_image)
            await ctx.send(f'Here\'s the karma trend for "{karma}" over time',
                           file=plot)
        else:
            # Construct the embed
            generated_at = datetime.strftime(
                utc.localize(datetime.utcnow()).astimezone(
                    timezone("Europe/London")),
                "%H:%M %d %b %Y",
            )
            time_taken = (t_end - t_start) / 1000
            total_changes = reduce(
                lambda count, size: count + size,
                map(lambda t: len(t[1]), karma_dict.items()),
                0,
            )
            # Construct the embed strings
            if karma_dict.keys():
                embed_colour = Color.from_rgb(61, 83, 255)
                embed_description = f'Tracked {len(karma_dict.keys())} {pluralise(karma_dict.keys(), "topic")} with a total of {total_changes} changes'
                embed_title = (
                    f"Karma trend over time for {comma_separate(list(karma_dict.keys()))}"
                    if len(karma_dict.keys()) == 1 else
                    f"Karma trends over time for {comma_separate(list(karma_dict.keys()))}"
                )
            else:
                embed_colour = Color.from_rgb(255, 23, 68)
                embed_description = f'The following {pluralise(failed, "problem")} occurred whilst plotting:'
                embed_title = f"Could not plot karma for {comma_separate(list(map(lambda i: i[0], failed)))}"
            embed = Embed(color=embed_colour,
                          title=embed_title,
                          description=embed_description)
            # If there were any errors then add them
            for karma, reason in failed:
                embed.add_field(name=f'Failed to plot "{karma}"',
                                value=f" • {reason}")

            # There was something plotted so attach the graph
            if karma_dict.keys():
                embed.set_footer(
                    text=
                    f"Graph generated at {generated_at} in {time_taken:.3f} seconds"
                )
                embed.set_image(url="{host}/{filename}".format(
                    host=CONFIG["FIG_HOST_URL"], filename=filename))

            display_name = get_name_string(ctx.message)
            await ctx.send(
                f"Here you go, {display_name}! :chart_with_upwards_trend:",
                embed=embed)
Ejemplo n.º 11
0
    async def info(self, ctx: Context, karma: clean_content):
        await ctx.trigger_typing()
        t_start = current_milli_time()
        # Strip any leading @s and get the item from the DB
        karma_stripped = karma.lstrip("@")
        karma_item = (db_session.query(KarmaModel).filter(
            func.lower(KarmaModel.name) == func.lower(karma_stripped)).first())

        # If the item doesn't exist then raise an error
        if not karma_item:
            raise KarmaError(
                message=f"\"{karma_stripped}\" hasn't been karma'd yet. :cry:")

        # Get the changes and plot the graph
        filename, path = await plot_karma({karma_stripped: karma_item.changes})

        # Get the user with the most karma
        # I'd use a group_by sql statement here but it seems to not terminate
        all_changes = (db_session.query(KarmaChange).filter(
            KarmaChange.karma_id == karma_item.id).order_by(
                KarmaChange.created_at.asc()).all())
        user_changes = defaultdict(list)
        for change in all_changes:
            user_changes[change.user].append(change)

        most_karma = max(user_changes.items(), key=lambda item: len(item[1]))

        # Calculate the approval rating of the karma
        approval = 100 * ((karma_item.pluses - karma_item.minuses) /
                          (karma_item.pluses + karma_item.minuses))
        mins_per_karma = (all_changes[-1].local_time -
                          all_changes[0].local_time).total_seconds() / (
                              60 * len(all_changes))
        time_taken = (current_milli_time() - t_start) / 1000

        # Attach the file as an image for dev purposes
        if CONFIG["DEBUG"]:
            # Attach the file as an image for dev purposes
            plot_image = open(path, mode="rb")
            plot = File(plot_image)
            await ctx.send(
                f'Here\'s the karma trend for "{karma_stripped}" over time',
                file=plot)
        else:
            # Construct the embed
            generated_at = datetime.strftime(
                utc.localize(datetime.utcnow()).astimezone(
                    timezone("Europe/London")),
                "%H:%M %d %b %Y",
            )
            embed_colour = Color.from_rgb(61, 83, 255)
            embed_title = f'Statistics for "{karma_stripped}"'
            embed_description = f'"{karma_stripped}" has been karma\'d {len(all_changes)} {pluralise(all_changes, "time")} by {len(user_changes.keys())} {pluralise(user_changes.keys(), "user")}.'

            embed = Embed(title=embed_title,
                          description=embed_description,
                          color=embed_colour)
            embed.add_field(
                name="Most karma'd",
                value=
                f'"{karma_stripped}" has been karma\'d the most by <@{most_karma[0].user_uid}> with a total of {len(most_karma[1])} {pluralise(most_karma[1], "change")}.',
            )
            embed.add_field(
                name="Approval rating",
                value=
                f'The approval rating of "{karma_stripped}" is {approval:.1f}% ({karma_item.pluses} positive to {karma_item.minuses} negative karma and {karma_item.neutrals} neutral karma).',
            )
            embed.add_field(
                name="Karma timeline",
                value=
                f'"{karma_stripped}" was first karma\'d on {datetime.strftime(all_changes[0].local_time, "%d %b %Y at %H:%M")} and has been karma\'d approximately every {mins_per_karma:.1f} minutes.',
            )
            embed.set_footer(
                text=
                f"Statistics generated at {generated_at} in {time_taken:.3f} seconds."
            )
            embed.set_image(url="{host}/{filename}".format(
                host=CONFIG["FIG_HOST_URL"], filename=filename))

            display_name = get_name_string(ctx.message)
            await ctx.send(f"Here you go, {display_name}! :page_facing_up:",
                           embed=embed)