Ejemplo n.º 1
0
 async def roll(self, ctx: Context, *, message: clean_content):
     display_name = get_name_string(ctx.message)
     if len(message) == 0:
         message = "1d6"
     message = message.strip()
     try:
         expression = DiceParser.parse_all.parse(message).or_die()
     except ParseError as e:
         await ctx.send(
             FAILURE_OUT.format(ping=display_name, body=f"```{e}```"))
         return
     except ExcessiveDiceRollsError:
         await ctx.send(
             WARNING_OUT.format(
                 ping=display_name,
                 body="_You requested an excessive number of dice rolls._",
             ))
         return
     value = expression.value
     try:
         out = SUCCESS_OUT.format(
             ping=display_name,
             body=
             f"`{repr(expression)}` | **{value}** ⟵ {clean_brackets(str(expression))}",
         )
         await ctx.send(out)
     except OutputTooLargeError:
         await ctx.send(
             WARNING_OUT.format(ping=display_name,
                                body="_Your output was too large!_"))
Ejemplo n.º 2
0
async def is_compsoc_exec_in_guild(ctx: Context):
    """Get the roles of the user in the UWCS discord"""
    # TODO: move the errors to call site? - checks should return false
    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.º 3
0
 async def eval(self, ctx: Context, *args: clean_content):
     display_name = get_name_string(ctx.message)
     self.errors = 0
     self.out = f"{display_name}:\n```"
     self.evaluate(" ".join(args))
     if self.errors:
         self.out += f"{self.errors} errors detected."
     self.out += "```"
     await ctx.send(f"{self.out}")
Ejemplo n.º 4
0
    async def roll(self, ctx: Context, *, message: clean_content):
        loop = asyncio.get_event_loop()
        display_name = get_name_string(ctx.message)

        def work():
            return run(message, display_name)

        p = await get_parallelism(self.bot)
        p.send_to_ctx_after_threaded(work, ctx, loop)
Ejemplo n.º 5
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.º 6
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.º 7
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.º 8
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 user_is_irc_bot(ctx):
                    author_id = 1
                    irc_n = display_name
                else:
                    author_id = (
                        db_session.query(User)
                        .filter(User.user_uid == ctx.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 (ScalarListException, SQLAlchemyError) as e:
                        db_session.rollback()
                        logging.error(e)
                        await ctx.send(f"Something went wrong")
                else:
                    await ctx.send("Please include some reminder text!")
Ejemplo n.º 9
0
 async def fact(self, ctx: Context):
     display_name = get_name_string(ctx.message)
     if json := self.get_online_fact():
         if user_is_irc_bot(ctx):
             await ctx.send(
                 f"{display_name}: {json['text']} (from <{json['source']}>)"
             )
         else:
             embed = Embed(
                 title=json["text"],
                 description=f'[{json["source"]}]({json["source"]})',
                 colour=Colour.random(),
             ).set_footer(text=json["index"])
             await ctx.send(embed=embed)
Ejemplo n.º 10
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 compsoc_member is None:
            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 = discord.utils.get(compsoc_member.roles, id=CONFIG['UWCS_EXEC_ROLE_ID'])
        if roles is None:
            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.º 11
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)
                    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).'
                    )
                else:
                    await ctx.send("Please include some reminder text!")
Ejemplo n.º 12
0
    async def add(self, ctx: Context, author: MentionConverter, *, quote):
        """Add a quote, format !quote add <author> <quote text>"""
        requester = get_name_string(ctx)
        now = datetime.now()

        try:
            quote_id = add_quote(author, quote, now)

            result = f"Thank you {requester}, recorded quote with ID #{quote_id}."
        except QuoteException as e:
            if e.err == QuoteError.BAD_FORMAT:
                result = "Invalid format: no quote to record."
            elif e.err == QuoteError.OPTED_OUT:
                result = "Invalid Author: User has opted out of being quoted."
            elif e.err == QuoteError.DB_ERROR:
                result = "Database error."
            else:
                result = MYSTERY_ERROR

        await ctx.send(result)
Ejemplo n.º 13
0
    async def add(self, ctx: Context, trigger_time: DateTimeConverter, *,
                  reminder_content: str):
        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 user_is_irc_bot(ctx):
                author_id = 1
                irc_n = display_name
            else:
                author_id = get_database_user(ctx.author).id
                irc_n = None

            trig_at = trigger_time
            trig = False
            playback_ch_id = ctx.message.channel.id
            new_reminder = Reminder(
                user_id=author_id,
                reminder_content=reminder_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 {precisedelta(CONFIG.REMINDER_SEARCH_INTERVAL, minimum_unit='seconds')})."
                )
            except (ScalarListException, SQLAlchemyError) as e:
                db_session.rollback()
                logging.exception(e)
                await ctx.send(f"Something went wrong")
Ejemplo n.º 14
0
 async def roll(self, ctx: Context, *, message: clean_content):
     display_name = get_name_string(ctx.message)
     if len(message) == 0:
         message = "1d6"
     message = message.strip()
     try:
         program = await self.parse(message)
         out = SUCCESS_OUT.format(
             ping=display_name,
             body=f"**{value(program)}** ⟵ `{clean_brackets(str(program))}`",
         )
         if len(out) > MAX_ROLLS:
             raise OutputTooLargeError
         await ctx.send(out)
     except WarningError as e:
         await ctx.send(
             WARNING_OUT.format(ping=display_name, body=f"_{e.out}_"))
     except (ParseError, RunTimeError) as e:
         await ctx.send(
             FAILURE_OUT.format(ping=display_name, body=f"```{e}```"))
     except (InternalError, Exception) as e:
         await ctx.send(
             INTERNAL_OUT.format(ping=display_name,
                                 body=f"**Internal error:**```{e}```"))
Ejemplo n.º 15
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.º 16
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)
Ejemplo n.º 17
0
def get_haotrac_report(my_df, freq):

    my_report = Performance_Report(my_df, freq)
    get_cum_ret_chart(my_report.cum_returns)
    """
    Define format specifications of the report.
    """
    x0 = 50
    y0 = 720
    w0 = 400  #Default width of text blocks
    h0 = 25  #Space between lines

    fontz_title = 20
    fontz_body = 12

    my_canvas = canvas.Canvas('./paper.pdf', pagesize=A4)

    # Cover
    my_canvas.setFontSize(fontz_title)
    my_canvas.drawCentredString(300, 570, 'Fund Performance Report')

    my_canvas.setFontSize(fontz_body)
    my_canvas.drawCentredString(
        300, 500, 'Fund Names: ' + get_name_string(my_report.fund_names))
    my_canvas.drawCentredString(300, 480,
                                'Benchmark Name: ' + my_report.benchmark_name)
    my_canvas.drawCentredString(
        300, 460, 'Start Date: ' + str(my_report.start_date)[0:10])
    my_canvas.drawCentredString(300, 440,
                                'End Date: ' + str(my_report.end_date)[0:10])
    my_canvas.drawCentredString(300, 380, 'Contents:')
    my_canvas.drawCentredString(300, 330, 'Cumulative Returns')
    my_canvas.drawCentredString(300, 310, 'Trailing Returns')
    my_canvas.drawCentredString(300, 290, 'Calendar Year Returns')
    my_canvas.drawCentredString(300, 270, 'Risk Return Scatterplot')
    my_canvas.drawCentredString(300, 250, 'Risk Table')
    my_canvas.drawCentredString(300, 230, 'Drawdown Analysis')
    my_canvas.drawCentredString(300, 210, 'Rolling Volatility')
    my_canvas.drawCentredString(300, 190, 'Rolling Beta')

    my_canvas.drawImage('./inputs/Qiyi logo.jpg',
                        225,
                        600,
                        width=150,
                        preserveAspectRatio=True)
    my_canvas.showPage()

    # Page 1
    my_canvas.setFontSize(fontz_title)
    my_canvas.drawString(x0, y0, '1. Cumulative Returns')
    my_canvas.drawString(x0, y0 - 370, '2. Trailing Period Returns')

    my_canvas.setFontSize(fontz_body)
    draw_table(my_canvas, my_report.trail_returns, x0 + 30, y0 - 410)

    my_canvas.drawImage('./inputs/cum_returns_chart.jpg',
                        x0,
                        y0 - 410,
                        width=400,
                        preserveAspectRatio=True)
    my_canvas.showPage()

    my_canvas.save()
Ejemplo n.º 18
0
def ctx_to_mention(ctx):
    """Convert requester name to Mention"""
    if user_is_irc_bot(ctx):
        return Mention.string_mention(get_name_string(ctx))
    else:
        return Mention.id_mention(get_database_user(ctx.author).id)
Ejemplo n.º 19
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.º 20
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.º 21
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.º 22
0
                          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)
            emoji = (":chart_with_upwards_trend:" if
                     sum(c.change for cs in karma_dict.values()
                         for c in cs) >= 0 else ":chart_with_downwards_trend:")
            await ctx.send(f"Here you go, {display_name}! {emoji}",
                           embed=embed)

    @plot.error
    async def plot_error_handler(self, ctx: Context, error: KarmaError):
        await ctx.send(error.message)

    @karma.command(help="Lists the reasons (if any) for the specific karma",
                   ignore_extra=True)
    async def reasons(self, ctx: Context, karma: clean_content):
        karma_stripped = karma.lstrip("@")
Ejemplo n.º 23
0
def process_karma(message: Message, message_id: int, db_session: Session,
                  timeout: int):
    reply = ""

    # Parse the message for karma modifications
    karma_items = parse_message_content(message.content)
    transactions = make_transactions(karma_items, message)
    transactions = apply_blacklist(transactions, db_session)

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

    # TODO: Protect from byte-limit length chars

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

    # Get whether the channel is on mini karma or not
    channel = (db_session.query(MiniKarmaChannel).filter(
        MiniKarmaChannel.channel == message.channel.id).one_or_none())
    if channel is None:
        karma_mode = MiniKarmaMode.Normal
    else:
        karma_mode = MiniKarmaMode.Mini

    def own_karma_error(topic):
        if karma_mode == MiniKarmaMode.Normal:
            return f' • Could not change "{topic}" because you cannot change your own karma! :angry:'
        else:
            return f'could not change "**{topic}**" (own name)'

    def internal_error(topic):
        if karma_mode == MiniKarmaMode.Normal:
            return f' • Could not create "{topic}" due to an internal error.'
        else:
            return f'could not change "**{topic}**" (internal error)'

    def cooldown_error(topic, td):
        # Tell the user that the item is on cooldown
        if td.seconds < 60:
            seconds_plural = f"second{'s' if td.seconds != 1 else ''}"
            duration = f"{td.seconds} {seconds_plural}"
        else:
            mins = td.seconds // 60
            mins_plural = f"minute{'s' if mins != 1 else ''}"
            duration = f"{mins} {mins_plural}"

        if karma_mode == MiniKarmaMode.Normal:
            return f' • Could not change "{topic}" since it is still on cooldown (last altered {duration} ago).\n'
        else:
            return (
                f'could not change "**{topic}**" (cooldown, last edit {duration} ago)'
            )

    def success_item(tr: KarmaTransaction):
        # Give some sass if someone is trying to downvote the bot
        if (tr.karma_item.topic.casefold() == "apollo"
                and tr.karma_item.operation.value < 0):
            apollo_response = ":wink:"
        else:
            apollo_response = ""

        op = str(tr.karma_item.operation)

        # Build the karma item string
        if tr.karma_item.reason:
            if karma_mode == MiniKarmaMode.Normal:
                if tr.self_karma:
                    return f" • **{truncated_name}** (new score is {karma_change.score}) and your reason has been recorded. *Fool!* that's less karma to you. :smiling_imp:"
                else:
                    return f" • **{truncated_name}** (new score is {karma_change.score}) and your reason has been recorded. {apollo_response}"
            else:
                return f"**{truncated_name}**{op} (now {karma_change.score}, reason recorded)"

        else:
            if karma_mode == MiniKarmaMode.Normal:
                if tr.self_karma:
                    return f" • **{truncated_name}** (new score is {karma_change.score}). *Fool!* that's less karma to you. :smiling_imp:"
                else:
                    return f" • **{truncated_name}** (new score is {karma_change.score}). {apollo_response}"
            else:
                return f"**{truncated_name}**{op} (now {karma_change.score})"

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

    items = []
    errors = []

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

        # Catch any self-karma transactions early
        if transaction.self_karma and transaction.karma_item.operation.value > -1:
            errors.append(own_karma_error(truncated_name))
            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.karma_item.topic)).one_or_none())

        # Update or create the karma item
        if not karma_item:
            karma_item = Karma(name=transaction.karma_item.topic)
            db_session.add(karma_item)
            try:
                db_session.commit()
            except (ScalarListException, SQLAlchemyError) as e:
                db_session.rollback()
                logging.error(e)
                errors.append(internal_error(truncated_name))
                continue

        # 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.karma_item.topic.casefold() == "apollo":
                new_score = abs(transaction.karma_item.operation.value)
            else:
                new_score = transaction.karma_item.operation.value

            karma_change = KarmaChange(
                karma_id=karma_item.id,
                user_id=user.id,
                message_id=message_id,
                reason=transaction.karma_item.reason,
                change=new_score,
                score=new_score,
                created_at=datetime.utcnow(),
            )
            db_session.add(karma_change)
            try:
                db_session.commit()
            except (ScalarListException, SQLAlchemyError) as e:
                db_session.rollback()
                logging.error(e)
                errors.append(internal_error(truncated_name))
                continue
        else:
            time_delta = datetime.utcnow() - last_change
            if is_in_cooldown(last_change, timeout):
                errors.append(cooldown_error(truncated_name, time_delta))
                continue

            # If the bot is being downvoted then the karma can only go up
            if transaction.karma_item.topic.casefold() == "apollo":
                new_score = last_change.score + abs(
                    transaction.karma_item.operation.value)
            else:
                new_score = last_change.score + transaction.karma_item.operation.value

            karma_change = KarmaChange(
                karma_id=karma_item.id,
                user_id=user.id,
                message_id=message_id,
                reasons=transaction.karma_item.reason,
                score=new_score,
                change=(new_score - last_change.score),
                created_at=datetime.utcnow(),
            )
            db_session.add(karma_change)
            try:
                db_session.commit()
            except (ScalarListException, SQLAlchemyError) as e:
                db_session.rollback()
                logging.error(e)
                errors.append(internal_error(truncated_name))
                karma_change = KarmaChange(
                    karma_id=karma_item.id,
                    user_id=user.id,
                    message_id=message_id,
                    reason=transaction.karma_item.reason,
                    score=new_score,
                    change=(new_score - last_change.score),
                    created_at=datetime.utcnow(),
                )
                db_session.add(karma_change)
                try:
                    db_session.commit()
                except (ScalarListException, SQLAlchemyError) as e:
                    db_session.rollback()
                    logging.error(e)
                    errors.append(internal_error(truncated_name))
                    continue
            else:
                errors.append(cooldown_error(truncated_name, time_delta))
                continue

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

        items.append(success_item(transaction))

    # 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 karma_mode == MiniKarmaMode.Normal:
        item_str = "\n".join(items)
        error_str = "\n".join(errors)
        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}"
    else:
        item_str = " ".join(items)
        error_str = " ".join(errors)
        reply = " ".join(filter(None, ["Changes:", item_str, error_str]))

    # Commit any changes (in case of any DB inconsistencies)
    try:
        db_session.commit()
    except (ScalarListException, SQLAlchemyError) as e:
        logging.error(e)
        db_session.rollback()
    return reply.rstrip()
Ejemplo n.º 24
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 = sum(len(v) for v in karma_dict.values())
            # 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([i[0] for i in 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)
            emoji = (":chart_with_upwards_trend"
                     if total_changes > 0 else ":chart_with_downwards_trend:")
            await ctx.send(f"Here you go, {display_name}! {emoji}",
                           embed=embed)
Ejemplo n.º 25
0
    async def tex(self, ctx: Context, *message: clean_content):
        await ctx.trigger_typing()
        print(message)
        # Input filtering
        if not message:
            await ctx.send("Your message contained nothing to render")

        if message[0] == "```tex":
            message = ("```", *message[1:])
        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")
            return

        tex_code = combined.lstrip("`").rstrip("`")
        # May want to consider enforcing the $message$ requirement here
        # May also want to disallow/convert $$message$$ environments here

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

        # Generate the filename
        filename = (tex_code + "-" +
                    str(hex(int(datetime.utcnow().timestamp()))).lstrip("0x") +
                    ".png").replace(" ", "")
        path_png = CONFIG.FIG_SAVE_PATH / filename
        path_jpg = path_png.with_suffix(".jpg")
        try:
            # Plot the latex and save it.
            plt.text(0, 1, tex_code, color="white")
            plt.savefig(path_png,
                        dpi=300,
                        bbox_inches="tight",
                        transparent=True)
        except RuntimeError as r:
            # Failed to render latex. Report error
            logging.error(r)
            await ctx.send(
                "Unable to render LaTeX. Please check that it's correct")
        else:
            # 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)