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
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)
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
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("@")}')
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
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!")
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)
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()
async def fact(self, ctx: Context): display_name = get_name_string(ctx.message) await ctx.send(f"{display_name}: {random.choice(self.options)}")
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)
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)