async def _quantize(self, ctx, args, canvas, palette): """Sends a message containing a quantised version of the image given. Arguments: ctx - A commands.Context object. args - A list of arguments from the user, all strings. canvas - The canvas to use, string. palette - The palette to quantise to, a list of rgb tuples. Returns: The discord.Message object returned when ctx.send() is called to send the quantised image. """ # Argument Parsing parser = GlimmerArgumentParser(ctx) parser.add_argument("-f", "--faction", default=None, action=FactionAction) # Pre-Parsing if len(args) == 0: name = None elif args[0][0] != "-": name = args[0] args = args[1:] else: name = None try: args = parser.parse_args(args) except TypeError: return log.debug(f"[uuid:{ctx.uuid}] Parsed arguments: {args}") gid = ctx.guild.id if not args.faction else args.faction.id t = ctx.session.query(Template).filter_by(guild_id=gid, name=name).first() data = None if name: if t: if t.canvas == canvas: raise IdempotentActionError data = await http.get_template(t.url, t.name) else: raise TemplateNotFoundError(ctx, gid, name) else: att = await verify_attachment(ctx) if att: data = io.BytesIO() await att.save(data) if data: template, bad_pixels = await self.bot.loop.run_in_executor(None, render.quantize, data, palette) with io.BytesIO() as bio: template.save(bio, format="PNG") bio.seek(0) f = discord.File(bio, "template.png") return await ctx.send(ctx.s("canvas.quantize").format(bad_pixels), file=f)
async def gridify(self, ctx, *args): log.info(f"g!gridify run in {ctx.guild.name} with args: {args}") # Argument Parsing parser = GlimmerArgumentParser(ctx) parser.add_argument("-f", "--faction", default=None, action=FactionAction) parser.add_argument("-c", "--color", default=0x808080, action=ColorAction) parser.add_argument("-z", "--zoom", type=int, default=1) # Pre-Parsing if len(args) == 0: name = None a = args elif args[0][0] != "-": name = args[0] a = args[1:] else: name = None a = args try: a = vars(parser.parse_args(a)) except TypeError: return faction = a["faction"] color = a["color"] zoom = a["zoom"] gid = ctx.guild.id if not faction else faction.id t = sql.template_get_by_name(gid, name) if name: if t: log.info("(T:{} | GID:{})".format(t.name, t.gid)) data = await http.get_template(t.url, t.name) max_zoom = int(math.sqrt(4000000 // (t.width * t.height))) zoom = max(1, min(zoom, max_zoom)) template = await render.gridify(data, color, zoom) else: raise TemplateNotFoundError(gid, name) else: att = await verify_attachment(ctx) data = io.BytesIO() await att.save(data) max_zoom = int(math.sqrt(4000000 // (att.width * att.height))) zoom = max(1, min(zoom, max_zoom)) template = await render.gridify(data, color, zoom) with io.BytesIO() as bio: template.save(bio, format="PNG") bio.seek(0) f = discord.File(bio, "gridded.png") await ctx.send(file=f)
def dither_argparse(ctx, args): parser = GlimmerArgumentParser(ctx) parser.add_argument( "-d", "--ditherType", choices=["b", "bayer", "y", "yliluoma", "fs", "floyd-steinberg"], default="bayer") parser.add_argument("-t", "--threshold", type=int, choices=[2, 4, 8, 16, 32, 64, 128, 256, 512]) parser.add_argument("-o", "--order", type=int, choices=[2, 4, 8, 16]) try: a = parser.parse_args(args) except TypeError: raise discord.ext.commands.BadArgument def default(value, default): return value if value is not None else default names = {"b": "bayer", "y": "yliluoma", "fs": "floyd-steinberg"} default_thresholds = { "bayer": 256, } default_orders = {"bayer": 4, "yliluoma": 8, "floyd-steinberg": 2} dither_type = default(names.get(a.ditherType, None), a.ditherType) dither_type = dither_type if dither_type is not None else "bayer" # Incase they select an invalid option for this threshold = default(a.threshold, default_thresholds.get(dither_type)) order = order = default(a.order, default_orders.get(dither_type)) return dither_type, threshold, order
async def _preview(ctx, args, fetch): """Sends a preview of the image provided. Arguments: ctx - A commands.Context object. args - A list of arguments from the user, all strings. fetch - The current state of all pixels that the template/specified area covers, PIL Image object. """ async with ctx.typing(): # Order Parsing try: x, y = args[0], args[1] except TypeError: await ctx.send("Error: no arguments were provided.") return if re.match("-\D+", x) != None: x, y = args[-2], args[-1] args = args[:-2] else: args = args[2:] # X and Y Cleanup try: #cleans up x and y by removing all spaces and chars that aren't 0-9 or the minus sign using regex. Then makes em ints x = int(re.sub('[^0-9-]', '', x)) y = int(re.sub('[^0-9-]', '', y)) except ValueError: await ctx.send(ctx.s("canvas.invalid_input")) return # Argument Parsing parser = GlimmerArgumentParser(ctx) parser.add_argument("-z", "--zoom", type=int, default=1) try: a = vars(parser.parse_args(args)) except TypeError: return zoom = a["zoom"] zoom = max(min(zoom, 16), -8) preview_img = await render.preview(x, y, zoom, fetch) with io.BytesIO() as bio: preview_img.save(bio, format="PNG") bio.seek(0) f = discord.File(bio, "preview.png") await ctx.send(file=f)
async def template(self, ctx, *args): # Argument Parsing parser = GlimmerArgumentParser(ctx) parser.add_argument("-p", "--page", type=int, default=1) parser.add_argument("-f", "--faction", default=None, action=FactionAction) try: args = parser.parse_args(args) except TypeError: return log.debug(f"[uuid:{ctx.uuid}] Parsed arguments: {args}") gid = ctx.guild.id if args.faction is not None: gid = args.faction.id templates = ctx.session.query(TemplateDb).filter_by( guild_id=gid).order_by(TemplateDb.name).all() if len(templates) < 1: raise NoTemplatesError() template_menu = menus.MenuPages(source=TemplateSource(templates), clear_reactions_after=True, timeout=300.0) template_menu.current_page = max( min(args.page - 1, template_menu.source.get_max_pages()), 0) try: await template_menu.start(ctx, wait=True) template_menu.source.embed.set_footer(text=ctx.s("bot.timeout")) await template_menu.message.edit(embed=template_menu.source.embed) except discord.NotFound: await ctx.send(ctx.s("bot.menu_deleted"))
async def gridify(self, ctx, *args): # Argument Parsing parser = GlimmerArgumentParser(ctx) parser.add_argument("-f", "--faction", default=None, action=FactionAction) parser.add_argument("-c", "--color", default=0x808080, action=ColorAction) parser.add_argument("-z", "--zoom", type=int, default=1) # Pre-Parsing if len(args) == 0: name = None a = args elif args[0][0] != "-": name = args[0] a = args[1:] else: name = None a = args try: a = parser.parse_args(a) except TypeError: return log.debug(f"[uuid:{ctx.uuid}] Parsed arguments: {a}") gid = ctx.guild.id if not a.faction else a.faction.id t = ctx.session.query(Template).filter_by(guild_id=gid, name=name).first() if name: if t: max_zoom = int(math.sqrt(4000000 // (t.width * t.height))) data = await http.get_template(t.url, t.name) zoom = max(1, min(a.zoom, max_zoom)) template = render.gridify(data, a.color, zoom) else: raise TemplateNotFoundError(ctx, gid, name) else: att = await verify_attachment(ctx) data = io.BytesIO() await att.save(data) max_zoom = int(math.sqrt(4000000 // (att.width * att.height))) zoom = max(1, min(a.zoom, max_zoom)) template = render.gridify(data, a.color, zoom) with io.BytesIO() as bio: template.save(bio, format="PNG") bio.seek(0) f = discord.File(bio, "gridded.png") await ctx.send(file=f)
async def recent(self, ctx, *args): # Argument Parsing parser = GlimmerArgumentParser(ctx) parser.add_argument("-p", "--page", type=int, default=1) parser.add_argument("-f", "--faction", default=None, action=FactionAction) try: args = parser.parse_args(args) except TypeError: return log.debug(f"[uuid:{ctx.uuid}] Parsed arguments: {args}") gid = ctx.guild.id if args.faction is not None: gid = args.faction.id templates = ctx.session.query(TemplateDb).filter_by(guild_id=gid).all() checker_templates = [ t for t in self.templates if t.id in [t_.id for t_ in templates] ] pixels = [ p for t in checker_templates for p in t.current_pixels if p.fixed is False ] pixels.sort(key=lambda p: p.recieved, reverse=True) if not pixels: await ctx.send(ctx.s("alerts.no_recent_errors")) return checker_menu = menus.MenuPages(source=CheckerSource( pixels, checker_templates), clear_reactions_after=True, timeout=300.0) checker_menu.current_page = max( min(args.page - 1, checker_menu.source.get_max_pages()), 0) try: await checker_menu.start(ctx, wait=True) checker_menu.source.embed.set_footer(text=ctx.s("bot.timeout")) await checker_menu.message.edit(embed=checker_menu.source.embed) except discord.NotFound: await ctx.send(ctx.s("bot.menu_deleted"))
async def template_update_pixelcanvas(self, ctx, *args): try: name = args[0] except TypeError: await ctx.send(ctx.s("error.missing_argument")) return skip = False for arg in args: if any(h == arg for h in ["--help", "-h"]): args = ["--help"] skip = True if not skip: if re.match(r"-\D+", name) is not None: name = args[-1] args = args[:-1] else: args = args[1:] orig_template = ctx.session.query(TemplateDb).filter_by( guild_id=ctx.guild.id, name=name).first() if not orig_template: raise TemplateNotFoundError(ctx, ctx.guild.id, name) # Argument Parsing parser = GlimmerArgumentParser(ctx) parser.add_argument("-n", "--newName", nargs="?", default=None) parser.add_argument("-x", nargs="?", default=None) parser.add_argument("-y", nargs="?", default=None) # if -i not present, False # if no value after -i, True # if value after -i, capture parser.add_argument("-i", "--image", nargs="?", const=True, default=None) try: args = parser.parse_args(args) except TypeError: return log.debug(f"[uuid:{ctx.uuid}] Parsed arguments: {args}") out = [] # Image is done first since I'm using the build_template method to update stuff, # and I don't want anything to have changed in orig_template before I use it if args.image: # Update image url = None if not isinstance(args.image, bool): url = args.image url = await select_url_update(ctx, url, out) if url is None: return # Sending the end is handled in select_url_update if it fails try: t = await build_template(ctx, orig_template.name, orig_template.x, orig_template.y, url, "pixelcanvas") except TemplateHttpError: out.append( ctx.s("template.url_access").format( ctx.s("template.update_file"))) return await send_end(ctx, out) except NoJpegsError: out.append( "Updating file failed: Seriously? A JPEG? Gross! Please create a PNG template instead." ) return await send_end(ctx, out) except NotPngError: out.append( "Updating file failed: That command requires a PNG image.") return await send_end(ctx, out) except (PilImageError, UrlError): out.append("{0}.".format(ctx.s("template.err.update_file"))) return await send_end(ctx, out) if t is None: out.append("{0}.".format(ctx.s("template.err.update_file"))) return await send_end(ctx, out) # update template data ctx.session.query(TemplateDb)\ .filter_by(guild_id=ctx.guild.id, name=name)\ .update({ "url": t.url, "md5": t.md5, "width": t.width, "height": t.height, "size": t.size, "date_modified": t.date_modified }) ctx.session.commit() out.append("File updated.") if args.x: orig_x = copy.copy(orig_template.x) try: x = int(re.sub('[^0-9-]', '', args.x)) except ValueError: out.append( "Updating x failed, value provided was not a number.") return await send_end(ctx, out) ctx.session.query(TemplateDb)\ .filter_by(guild_id=ctx.guild.id, name=name)\ .update({ "x": x, "date_modified": int(time.time()) }) ctx.session.commit() out.append(f"X coordinate changed from {orig_x} to {x}.") if args.y: orig_y = copy.copy(orig_template.y) try: y = int(re.sub('[^0-9-]', '', args.y)) except ValueError: out.append( "Updating y failed, value provided was not a number.") return await send_end(ctx, out) ctx.session.query(TemplateDb)\ .filter_by(guild_id=ctx.guild.id, name=name)\ .update({ "y": y, "date_modified": int(time.time()) }) ctx.session.commit() out.append(f"Y coordinate changed from {orig_y} to {y}.") if args.newName: dup_check = ctx.session.query(TemplateDb.name).filter_by( guild_id=ctx.guild.id, name=args.newName).first() if dup_check is not None: out.append( f"Updating name failed, the name {args.newName} is already in use." ) return await send_end(ctx, out) if len(args.newName) > config.MAX_TEMPLATE_NAME_LENGTH: out.append("Updating name failed: {}".format( ctx.s("template.err.name_too_long").format( config.MAX_TEMPLATE_NAME_LENGTH))) return await send_end(ctx, out) if args.newName[0] == "-": out.append( "Updating name failed: Names cannot begin with hyphens.") return await send_end(ctx, out) try: _ = int(args.newName) out.append("Updating name failed: Names cannot be numbers.") return await send_end(ctx, out) except ValueError: pass ctx.session.query(TemplateDb)\ .filter_by(guild_id=ctx.guild.id, name=name)\ .update({ "name": args.newName, "date_modified": int(time.time()) }) ctx.session.commit() out.append(f"Nickname changed from {name} to {args.newName}.") await send_end(ctx, out)
async def template_info(self, ctx, *args): # Order Parsing try: name = args[0] except TypeError: await ctx.send("Error: no arguments were provided.") return if re.match("-\D+", name) != None: name = args[-1] args = args[:-1] else: args = args[1:] # Argument Parsing parser = GlimmerArgumentParser(ctx) parser.add_argument("-r", "--raw", action="store_true") parser.add_argument("-f", "--faction", default=None, action=FactionAction) parser.add_argument("-z", "--zoom", default=1) try: args = vars(parser.parse_args(args)) except TypeError: return image_only = args["raw"] f = args["faction"] try: gid, faction = f.id, f except AttributeError: gid, faction = ctx.guild.id, sql.guild_get_by_id(ctx.guild.id) zoom = args["zoom"] t = sql.template_get_by_name(gid, name) if not t: raise TemplateNotFoundError(gid, name) if image_only: try: if type(zoom) is not int: if zoom.startswith("#"): zoom = zoom[1:] zoom = int(zoom) except ValueError: zoom = 1 max_zoom = int(math.sqrt(4000000 // (t.width * t.height))) zoom = max(1, min(zoom, max_zoom)) img = render.zoom(await http.get_template(t.url, t.name), zoom) with io.BytesIO() as bio: img.save(bio, format="PNG") bio.seek(0) f = discord.File(bio, t.name + ".png") await ctx.send(file=f) return canvas_name = canvases.pretty_print[t.canvas] coords = "{}, {}".format(t.x, t.y) dimensions = "{} x {}".format(t.width, t.height) size = t.size visibility = ctx.s("bot.private") if bool( t.private) else ctx.s("bot.public") owner = self.bot.get_user(t.owner_id) if owner is None: added_by = ctx.s("error.account_deleted") else: added_by = owner.name + "#" + owner.discriminator date_added = datetime.date.fromtimestamp( t.date_created).strftime("%d %b, %Y") date_modified = datetime.date.fromtimestamp( t.date_updated).strftime("%d %b, %Y") color = faction.faction_color description = "[__{}__]({})".format( ctx.s("template.link_to_canvas"), canvases.url_templates[t.canvas].format(*t.center())) if size == 0: t.size = await render.calculate_size(await http.get_template( t.url, t.name)) sql.template_update(t) e = discord.Embed(title=t.name, color=color, description=description) \ .set_image(url=t.url) \ .add_field(name=ctx.s("bot.canvas"), value=canvas_name, inline=True) \ .add_field(name=ctx.s("bot.coordinates"), value=coords, inline=True) \ .add_field(name=ctx.s("bot.dimensions"), value=dimensions, inline=True) \ .add_field(name=ctx.s("bot.size"), value=size, inline=True) \ .add_field(name=ctx.s("bot.visibility"), value=visibility, inline=True) \ .add_field(name=ctx.s("bot.added_by"), value=added_by, inline=True) \ .add_field(name=ctx.s("bot.date_added"), value=date_added, inline=True) \ .add_field(name=ctx.s("bot.date_modified"), value=date_modified, inline=True) if faction.id != ctx.guild.id and faction.faction_name: e = e.set_author(name=faction.faction_name, icon_url=faction.faction_emblem or discord.Embed.Empty) await ctx.send(embed=e)
async def template(self, ctx, *args): # Argument Parsing parser = GlimmerArgumentParser(ctx) parser.add_argument("-p", "--page", type=int, default=1) parser.add_argument("-f", "--faction", default=None, action=FactionAction) try: args = vars(parser.parse_args(args)) except TypeError: return page = args["page"] faction = args["faction"] gid = ctx.guild.id if faction != None: gid = faction.id templates = sql.template_get_all_by_guild_id(gid) if len(templates) < 1: raise NoTemplatesError() # Find number of pages given there are 25 templates per page. pages = int(math.ceil(len(templates) / 25)) # Makes sure page is in the range (1 <= page <= pages). page = min(max(page, 0), pages) page_index = page - 1 embed = Template.build_table(ctx, page_index, pages, templates) message = await ctx.send(embed=embed) await message.add_reaction('◀') await message.add_reaction('▶') def is_valid(reaction, user): return reaction.message.id == message.id and ( reaction.emoji == '◀' or reaction.emoji == '▶') and user.id != discord.ClientUser.id _5_minutes_in_future = (datetime.datetime.today() + datetime.timedelta(minutes=5.0)) try: while _5_minutes_in_future > datetime.datetime.today(): add_future = asyncio.ensure_future( self.bot.wait_for("reaction_add", timeout=300.0, check=is_valid)) remove_future = asyncio.ensure_future( self.bot.wait_for("reaction_remove", timeout=300.0, check=is_valid)) reaction, _user = None, None while True: if remove_future.done() == True: reaction, _user = remove_future.result() break if add_future.done() == True: reaction, _user = add_future.result() break await asyncio.sleep(0.1) if reaction.emoji == '◀': if page_index != 0: #not on first page, scroll left page_index -= 1 embed = Template.build_table(ctx, page_index, pages, templates) await message.edit(embed=embed) elif reaction.emoji == '▶': if page_index != pages - 1: #not on last page, scroll right page_index += 1 embed = Template.build_table(ctx, page_index, pages, templates) await message.edit(embed=embed) except asyncio.TimeoutError: pass await message.edit(content=ctx.s("bot.timeout"), embed=embed)
async def template_update_pixelcanvas(self, ctx, *args): log.info(f"g!t update run in {ctx.guild.name} with args: {args}") try: name = args[0] except TypeError: await ctx.send( "Template not updated as no arguments were provided.") return if re.match("-\D+", name) != None: name = args[-1] args = args[:-1] else: args = args[1:] orig_template = sql.template_get_by_name(ctx.guild.id, name) if not orig_template: raise TemplateNotFoundError(ctx.guild.id, name) # Argument Parsing parser = GlimmerArgumentParser(ctx) parser.add_argument("-n", "--newName", nargs="?", default=None) parser.add_argument("-x", nargs="?", default=None) parser.add_argument("-y", nargs="?", default=None) # if -i not present, False # if no value after -i, True # if value after -i, capture parser.add_argument("-i", "--image", nargs="?", const=True, default=None) try: args = vars(parser.parse_args(args)) except TypeError: return new_name = args["newName"] x = args["x"] y = args["y"] image = args["image"] out = [] """Image is done first since I'm using the build_template method to update stuff, and I don't want anything to have changed in orig_template before I use it""" if image: # Update image url = None if not isinstance(image, bool): url = image url = await Template.select_url_update(ctx, url, out) if url is None: return # Sending the end is handled in select_url_update if it fails try: t = await Template.build_template(ctx, orig_template.name, orig_template.x, orig_template.y, url, "pixelcanvas") except TemplateHttpError: out.append( f"Updating file failed: Could not access URL for template." ) await Template.send_end(ctx, out) return except NoJpegsError: out.append( f"Updating file failed: Seriously? A JPEG? Gross! Please create a PNG template instead." ) await Template.send_end(ctx, out) return except NotPngError: out.append( f"Updating file failed: That command requires a PNG image." ) await Template.send_end(ctx, out) return except (PilImageError, UrlError): out.append(f"Updating file failed.") await Template.send_end(ctx, out) return if t is None: out.append(f"Updating file failed.") await Template.send_end(ctx, out) return # Could check for md5 duplicates here, maybe implement that later sql.template_kwarg_update(ctx.guild.id, orig_template.name, url=t.url, md5=t.md5, w=t.width, h=t.height, size=t.size, date_modified=int(time.time())) out.append(f"File updated.") if x: # Update x coord try: x = int(re.sub('[^0-9-]', '', x)) except ValueError: out.append( "Updating x failed, value provided was not a number.") await Template.send_end(ctx, out) return sql.template_kwarg_update(ctx.guild.id, orig_template.name, x=x, date_modified=int(time.time())) out.append(f"X coordinate changed from {orig_template.x} to {x}.") if y: # Update y coord try: y = int(re.sub('[^0-9-]', '', y)) except ValueError: out.append( "Updating y failed, value provided was not a number.") await Template.send_end(ctx, out) return sql.template_kwarg_update(ctx.guild.id, orig_template.name, y=y, date_modified=int(time.time())) out.append(f"Y coordinate changed from {orig_template.y} to {y}.") if new_name: # Check if new name is already in use dup_check = sql.template_get_by_name(ctx.guild.id, new_name) if dup_check != None: out.append( f"Updating name failed, the name {new_name} is already in use." ) await Template.send_end(ctx, out) return # Check if new name is too long if len(new_name) > config.MAX_TEMPLATE_NAME_LENGTH: out.append("Updating name failed: " + ctx.s("template.err.name_too_long").format( config.MAX_TEMPLATE_NAME_LENGTH)) await Template.send_end(ctx, out) return # Check if new name begins with a '-' if new_name[0] == "-": out.append( "Updating name failed: Names cannot begin with hyphens.") await Template.send_end(ctx, out) return # Make sure the name isn't a number try: c = int(new_name) out.append("Updating name failed: Names cannot be numbers.") await Template.send_end(ctx, out) return except ValueError: pass # None with new nick, update template sql.template_kwarg_update(ctx.guild.id, orig_template.name, new_name=new_name, date_modified=int(time.time())) out.append(f"Nickname changed from {name} to {new_name}.") await Template.send_end(ctx, out)
async def _quantize(ctx, args, canvas, palette): """Sends a message containing a quantised version of the image given. Arguments: ctx - A commands.Context object. args - A list of arguments from the user, all strings. canvas - The canvas to use, string. palette - The palette to quantise to, a list of rgb tuples. Returns: The discord.Message object returned when ctx.send() is called to send the quantised image. """ # Argument Parsing parser = GlimmerArgumentParser(ctx) parser.add_argument("-f", "--faction", default=None, action=FactionAction) parser.add_argument("-z", "--zoom", type=int, default=1) # Pre-Parsing if len(args) == 0: name = None elif args[0][0] != "-": name = args[0] args = args[1:] else: name = None try: args = vars(parser.parse_args(args)) except TypeError: return faction = args["faction"] zoom = args["zoom"] gid = ctx.guild.id if not faction else faction.id t = sql.template_get_by_name(gid, name) data = None if name: if t: log.info("(T:{} | GID:{})".format(t.name, t.gid)) if t.canvas == canvas: raise IdempotentActionError data = await http.get_template(t.url, t.name) else: raise TemplateNotFoundError(gid, name) else: att = await verify_attachment(ctx) if att: data = io.BytesIO() await att.save(data) if data: template, bad_pixels = await render.quantize(data, palette) with io.BytesIO() as bio: template.save(bio, format="PNG") bio.seek(0) f = discord.File(bio, "template.png") return await ctx.send(ctx.s("canvas.quantize").format(bad_pixels), file=f)
async def _pre_diff(self, ctx, args, name=None, canvas=None, fetch=None, palette=None, help=False): for arg in args: if any(h == arg for h in ["--help", "-h"]): help = True if not help and not name: att = await verify_attachment(ctx) # Order Parsing try: x, y = args[0], args[1] except IndexError: await ctx.send("Error: not enough arguments were provided.") return if re.match(r"-\D+", x) is not None: x, y = args[-2], args[-1] args = args[:-2] else: args = args[2:] # X and Y Cleanup try: # cleans up x and y by removing all spaces and chars that aren't 0-9 or the minus sign using regex. Then makes em ints x = int(re.sub('[^0-9-]', '', x)) y = int(re.sub('[^0-9-]', '', y)) except ValueError: await ctx.send(ctx.s("canvas.invalid_input")) return # Argument Parsing parser = GlimmerArgumentParser(ctx) parser.add_argument("-e", "--errors", action='store_true') parser.add_argument("-s", "--snapshot", action='store_true') parser.add_argument("-c", "--highlightCorrect", action='store_true') parser.add_argument("-cb", "--colorBlind", action='store_true') parser.add_argument("-z", "--zoom", type=int, default=1) parser.add_argument("-t", "--excludeTarget", action='store_true') colorFilters = parser.add_mutually_exclusive_group() colorFilters.add_argument("-ec", "--excludeColors", nargs="+", type=int, default=None) colorFilters.add_argument("-oc", "--onlyColors", nargs="+", type=int, default=None) if name: parser.add_argument("-f", "--faction", default=None, action=FactionAction) try: if help: args = ["--help"] args = parser.parse_args(args) except TypeError: return log.debug(f"[uuid:{ctx.uuid}] Parsed arguments: {args}") if name: gid = ctx.guild.id if not args.faction else args.faction.id t = ctx.session.query(Template).filter_by(guild_id=gid, name=name).first() if t: data = await http.get_template(t.url, t.name) await self._diff( ctx, args, t.x, t.y, t.width, t.height, t.canvas, self.bot.fetchers[t.canvas], colors.by_name[t.canvas], data) else: raise TemplateNotFoundError(ctx, gid, name) else: data = io.BytesIO() await att.save(data) await self._diff( ctx, args, x, y, att.width, att.height, canvas, fetch, palette, data)
async def diff(self, ctx, *args): log.info(f"g!diff run in {ctx.guild.name} with args: {args}") # Order Parsing try: name = args[0] except TypeError: await ctx.send("Error: no arguments were provided.") return if re.match("-\D+", name) != None: name = args[-1] args = args[:-1] else: args = args[1:] if re.match("-{0,1}\d+", name) != None: # Skip to coords + image parsing await ctx.invoke_default("diff") return # Argument Parsing parser = GlimmerArgumentParser(ctx) parser.add_argument("-e", "--errors", action='store_true') parser.add_argument("-s", "--snapshot", action='store_true') parser.add_argument("-f", "--faction", default=None, action=FactionAction) parser.add_argument("-z", "--zoom", type=int, default=1) parser.add_argument("-t", "--excludeTarget", action='store_true') colorFilters = parser.add_mutually_exclusive_group() colorFilters.add_argument("-ec", "--excludeColors", nargs="+", type=int, default=None) colorFilters.add_argument("-oc", "--onlyColors", nargs="+", type=int, default=None) try: a = vars(parser.parse_args(args)) except TypeError: return list_pixels = a["errors"] create_snapshot = a["snapshot"] faction = a["faction"] zoom = a["zoom"] exclude_target = a["excludeTarget"] exclude_colors = a["excludeColors"] only_colors = a["onlyColors"] gid = ctx.guild.id if not faction else faction.id t = sql.template_get_by_name(gid, name) if t: async with ctx.typing(): log.info("(T:{} | GID:{})".format(t.name, t.gid)) data = await http.get_template(t.url, t.name) max_zoom = int(math.sqrt(4000000 // (t.width * t.height))) zoom = max(1, min(zoom, max_zoom)) fetchers = { 'pixelcanvas': render.fetch_pixelcanvas, 'pixelzone': render.fetch_pixelzone, 'pxlsspace': render.fetch_pxlsspace } diff_img, tot, err, bad, err_list \ = await render.diff(t.x, t.y, data, zoom, fetchers[t.canvas], colors.by_name[t.canvas], create_snapshot) done = tot - err perc = done / tot if perc < 0.00005 and done > 0: perc = ">0.00%" elif perc >= 0.99995 and err > 0: perc = "<100.00%" else: perc = "{:.2f}%".format(perc * 100) out = ctx.s("canvas.diff") if bad == 0 else ctx.s( "canvas.diff_bad_color") out = out.format(done, tot, err, perc, bad=bad) with io.BytesIO() as bio: diff_img.save(bio, format="PNG") bio.seek(0) f = discord.File(bio, "diff.png") await ctx.send(content=out, file=f) if list_pixels and len(err_list) > 0: error_list = [] for x, y, current, target in err_list: # Color Filtering c = current if not exclude_target else target if exclude_colors: if c in exclude_colors: continue elif only_colors: if not c in only_colors: continue # The current x,y are in terms of the template area, add to template start coords so they're in terms of canvas x += t.x y += t.y error_list.append(Pixel(current, target, x, y)) checker = Checker(self.bot, ctx, t.canvas, error_list) checker.connect_websocket() else: # No template found raise TemplateNotFoundError(gid, name)
async def check(self, ctx, *args): # Argument Parsing parser = GlimmerArgumentParser(ctx) parser.add_argument("-e", "--onlyErrors", action='store_true') parser.add_argument("-f", "--faction", default=None, action=FactionAction) parser.add_argument("-s", "--sort", default="name_az", choices=[ "name_az", "name_za", "errors_az", "errors_za", "percent_az", "percent_za" ]) try: a = vars(parser.parse_args(args)) except TypeError: return only_errors = a["onlyErrors"] faction = a["faction"] sort = a["sort"] if faction: templates = sql.template_get_all_by_guild_id(faction.id) else: templates = sql.template_get_all_by_guild_id(ctx.guild.id) if len(templates) < 1: ctx.command.parent.reset_cooldown(ctx) raise NoTemplatesError(False) msg = None # Calc info + send temp msg for canvas, canvas_ts in itertools.groupby(templates, lambda tx: tx.canvas): ct = list(canvas_ts) msg = await check_canvas(ctx, ct, canvas, msg=msg) # Delete temp msg and send final report await msg.delete() ts = [t for t in templates if t.errors != 0] if only_errors else templates if sort == "name_az" or sort == "name_za": ts = sorted(ts, key=lambda t: t.name, reverse=(sort == "name_za")) elif sort == "errors_az" or sort == "errors_za": ts = sorted(ts, key=lambda t: t.errors, reverse=(sort == "errors_za")) elif sort == "percent_az" or sort == "percent_za": ts = sorted(ts, key=lambda t: (t.size - t.errors) / t.size, reverse=(sort == "percent_za")) ts = sorted(ts, key=lambda t: t.canvas) # Find number of pages given there are 25 templates per page. pages = int(math.ceil(len(ts) / 25)) await build_template_report(ctx, ts, None, pages)
async def preview(self, ctx, *args): log.info(f"g!preview run in {ctx.guild.name} with args: {args}") # Order Parsing try: name = args[0] except TypeError: await ctx.send("Error: no arguments were provided.") return if re.match("-\D+", name) != None: name = args[-1] args = args[:-1] else: args = args[1:] if re.match("-{0,1}\d+", name) != None: # Skip to coords + image parsing await ctx.invoke_default("preview") return # Argument Parsing parser = GlimmerArgumentParser(ctx) parser.add_argument("-t", "--templateRegion", action='store_true') parser.add_argument("-f", "--faction", default=None, action=FactionAction) parser.add_argument("-z", "--zoom", type=int, default=1) try: a = vars(parser.parse_args(args)) except TypeError: return preview_template_region = a["templateRegion"] faction = a["faction"] zoom = a["zoom"] gid = ctx.guild.id if not faction else faction.id t = sql.template_get_by_name(gid, name) if t: async with ctx.typing(): log.info("(T:{} | GID:{})".format(t.name, t.gid)) max_zoom = int(math.sqrt(4000000 // (t.width * t.height))) zoom = max(-8, min(zoom, max_zoom)) fetchers = { 'pixelcanvas': render.fetch_pixelcanvas, 'pixelzone': render.fetch_pixelzone, 'pxlsspace': render.fetch_pxlsspace } if preview_template_region: preview_img = await render.preview(*t.center(), zoom, fetchers[t.canvas]) else: preview_img = await render.preview_template( t, zoom, fetchers[t.canvas]) with io.BytesIO() as bio: preview_img.save(bio, format="PNG") bio.seek(0) f = discord.File(bio, "preview.png") await ctx.send(file=f) return # No template found raise TemplateNotFoundError(gid, name)
async def _preview(self, ctx, args, name=None, fetch=None, help=False): """Sends a preview of the image or template provided. Arguments: ctx - A commands.Context object. args - A list of arguments from the user, all strings. Keyword Arguments: name - The name of the template to preview. fetch - A function to fetch from a specific canvas. """ for arg in args: if any(h == arg for h in ["--help", "-h"]): help = True if not help and not name: # Order Parsing try: x, y = args[0], args[1] except IndexError: await ctx.send("Error: no arguments were provided.") return if re.match(r"-\D+", x) is not None: x, y = args[-2], args[-1] args = args[:-2] else: args = args[2:] # X and Y Cleanup try: # Remove all spaces and chars that aren't 0-9 or the minus sign. x = int(re.sub('[^0-9-]', '', x)) y = int(re.sub('[^0-9-]', '', y)) except ValueError: await ctx.send(ctx.s("canvas.invalid_input")) return # Argument Parsing parser = GlimmerArgumentParser(ctx) parser.add_argument("-z", "--zoom", type=int, default=1) if name: parser.add_argument("-t", "--templateRegion", action='store_true') parser.add_argument("-f", "--faction", default=None, action=FactionAction) try: if help: args = ["--help"] args = parser.parse_args(args) except TypeError: return log.debug(f"[uuid:{ctx.uuid}] Parsed arguments: {args}") t = None if name: gid = ctx.guild.id if not args.faction else args.faction.id t = ctx.session.query(Template).filter_by(guild_id=gid, name=name).first() if not t: raise TemplateNotFoundError(ctx, gid, name) fetch = self.bot.fetchers[t.canvas] if args.templateRegion: x, y = t.center async with ctx.typing(): zoom = max(min(args.zoom, 16), -8) if t: preview_img = await render.preview_template(self.bot, t, zoom, fetch) else: preview_img = await render.preview(self.bot, x, y, zoom, fetch) with io.BytesIO() as bio: preview_img.save(bio, format="PNG") bio.seek(0) f = discord.File(bio, "preview.png") await ctx.send(file=f)
async def template_info(self, ctx, *args): # Order Parsing try: name = args[0] except IndexError: return await ctx.send(ctx.s("error.missing_argument")) skip = False for arg in args: if any(h == arg for h in ["--help", "-h"]): args = ["--help"] skip = True if not skip: if re.match(r"-\D+", name) is not None: name = args[-1] args = args[:-1] else: args = args[1:] # Argument Parsing parser = GlimmerArgumentParser(ctx) parser.add_argument("-r", "--raw", action="store_true") parser.add_argument("-f", "--faction", default=None, action=FactionAction) parser.add_argument("-z", "--zoom", default=1) try: args = parser.parse_args(args) except TypeError: return log.debug(f"[uuid:{ctx.uuid}] Parsed arguments: {args}") try: gid, faction = args.faction.id, args.faction except AttributeError: gid, faction = ctx.guild.id, ctx.session.query(Guild).get( ctx.guild.id) t = ctx.session.query(TemplateDb).filter_by(guild_id=gid, name=name).first() if not t: raise TemplateNotFoundError(ctx, gid, name) if args.raw: try: zoom = int(args.zoom) except ValueError: zoom = 1 max_zoom = int(math.sqrt(4000000 // (t.width * t.height))) zoom = max(1, min(zoom, max_zoom)) img = render.zoom(await http.get_template(t.url, t.name), zoom) with io.BytesIO() as bio: img.save(bio, format="PNG") bio.seek(0) f = discord.File(bio, t.name + ".png") await ctx.send(file=f) return canvas_name = canvases.pretty_print[t.canvas] coords = "{}, {}".format(t.x, t.y) dimensions = "{} x {}".format(t.width, t.height) size = t.size owner = self.bot.get_user(t.owner) if owner is None: added_by = ctx.s("error.account_deleted") else: added_by = owner.name + "#" + owner.discriminator date_added = datetime.date.fromtimestamp( t.date_added).strftime("%d %b, %Y") date_modified = datetime.date.fromtimestamp( t.date_modified).strftime("%d %b, %Y") color = faction.faction_color description = "[__{}__]({})".format( ctx.s("template.link_to_canvas"), canvases.url_templates[t.canvas].format(*t.center)) e = discord.Embed(title=t.name, color=color, description=description) \ .set_image(url=t.url) \ .add_field(name=ctx.s("bot.canvas"), value=canvas_name, inline=True) \ .add_field(name=ctx.s("bot.coordinates"), value=coords, inline=True) \ .add_field(name=ctx.s("bot.dimensions"), value=dimensions, inline=True) \ .add_field(name=ctx.s("bot.size"), value=size, inline=True) \ .add_field(name=ctx.s("bot.added_by"), value=added_by, inline=True) \ .add_field(name=ctx.s("bot.date_added"), value=date_added, inline=True) \ .add_field(name=ctx.s("bot.date_modified"), value=date_modified, inline=True) if t.alert_id: channel = self.bot.get_channel(t.alert_id) e.add_field(name=ctx.s("bot.alert_channel"), value=channel.mention, inline=True) if faction.id != ctx.guild.id and faction.faction_name: e = e.set_author(name=faction.faction_name, icon_url=faction.faction_emblem or discord.Embed.Empty) await ctx.send(embed=e)
async def check(self, ctx, *args): # Argument Parsing parser = GlimmerArgumentParser(ctx) parser.add_argument("-e", "--onlyErrors", action='store_true') parser.add_argument("-f", "--faction", default=None, action=FactionAction) parser.add_argument("-s", "--sort", default="name_az", choices=[ "name_az", "name_za", "errors_az", "errors_za", "percent_az", "percent_za"]) parser.add_argument("-p", "--page", default=1, type=int) try: a = parser.parse_args(args) except TypeError: return log.debug(f"[uuid:{ctx.uuid}] Parsed arguments: {a}") if a.faction: templates = ctx.session.query(Template).filter_by(guild_id=a.faction.id).all() else: templates = ctx.session.query(Template).filter_by(guild_id=ctx.guild.id).all() if len(templates) < 1: ctx.command.reset_cooldown(ctx) raise NoTemplatesError(False) msg = None # Calc info + send temp msg for canvas, canvas_ts in itertools.groupby(templates, lambda tx: tx.canvas): ct = list(canvas_ts) msg = await self.check_canvas(ctx, ct, canvas, msg=msg) # Delete temp msg and send final report await msg.delete() ts = [t for t in templates if t.errors != 0] if a.onlyErrors else templates if a.sort == "name_az" or a.sort == "name_za": ts = sorted(ts, key=lambda t: t.name, reverse=(a.sort == "name_za")) elif a.sort == "errors_az" or a.sort == "errors_za": ts = sorted(ts, key=lambda t: t.errors, reverse=(a.sort == "errors_za")) elif a.sort == "percent_az" or a.sort == "percent_za": ts = sorted(ts, key=lambda t: (t.size - t.errors) / t.size, reverse=(a.sort == "percent_za")) ts = sorted(ts, key=lambda t: t.canvas) check_menu = menus.MenuPages( source=CheckSource(ts), clear_reactions_after=True, timeout=300.0) check_menu.current_page = max(min(a.page - 1, check_menu.source.get_max_pages()), 0) try: await check_menu.start(ctx, wait=True) check_menu.source.embed.set_footer(text=ctx.s("bot.timeout")) await check_menu.message.edit(embed=check_menu.source.embed) except discord.NotFound: await ctx.send(ctx.s("bot.menu_deleted"))
async def send_stats(self, ctx, args, canvas): parser = GlimmerArgumentParser(ctx) output = parser.add_mutually_exclusive_group() output.add_argument( "-t", "--type", default="hexbin", choices=["color-pie", "hexbin", "online-line", "2dhist", "placement-hist"]) output.add_argument("-r", "--raw", default=False, choices=["placement", "online"]) parser.add_argument( "-d", "--duration", default=DurationAction.get_duration(ctx, "1d"), action=DurationAction) parser.add_argument("-c", "--center", nargs=2, type=int) parser.add_argument("-a", "--radius", type=int, default=500) parser.add_argument("--nooverlay", action="store_true") parser.add_argument("--bins", default="log", choices=["log", "count"]) parser.add_argument("--mean", action="store_true") parser.add_argument( "--colormap", default="plasma", choices=[cmap for cmap in cmaps.keys() if not cmap.endswith("_r")], help="See: https://matplotlib.org/tutorials/colors/colormaps.html for visualisations.") try: args = parser.parse_args(args) except TypeError: return # Verify coordinate info. if args.center: center = args.center else: center = canvases.center[canvas] if args.radius > 1000: return await ctx.send(ctx.s(canvas.radius_toolarge).format(1000)) start_x, start_y = center[0] - args.radius, center[1] - args.radius end_x, end_y = center[0] + args.radius, center[1] + args.radius axes = [start_x, end_x, start_y, end_y] start = args.duration.start end = args.duration.end start_str = args.duration.start.strftime("%d %b %Y %H:%M:%S UTC") end_str = args.duration.end.strftime("%d %b %Y %H:%M:%S UTC") log.debug(f"[uuid:{ctx.uuid}] Parsed arguments: {args}") # NOTE: Could definitely think about using processes rather than threads # for both the collection+processing and plotting of our data here. # I'm pretty sure none of these functions are actually sharing sqlalchemy # objects across (minus the session, but we can just ditch that and access # the db via the engine directly after calling engine.dispose()). # Threading means we don't block the event loop, but it's gonna for sure slow # stuff down. if args.raw: process_func = partial(plot.process_raw, ctx, canvas, args.duration.start, args.duration.end, args.raw) buf = await self.bot.loop.run_in_executor(None, process_func) content = ctx.s(f"canvas.csv_{args.raw}").format( start_str, end_str, canvases.pretty_print[canvas]) file = discord.File(buf, "{0}-from-{1}-to-{2}.csv".format( canvas, int(args.duration.start.timestamp()), int(args.duration.end.timestamp()))) await ctx.send(content, file=file) return if args.type == "color-pie": process_func = partial(plot.process_color_pie, canvas, args.duration.start, args.duration.end) data = await self.bot.loop.run_in_executor(None, process_func) plot_func = partial(plot.color_pie, data, canvas) image = await self.bot.loop.run_in_executor(None, plot_func) content = ctx.s("canvas.pie_color_title").format( canvases.pretty_print[canvas], start_str, end_str) elif args.type == "hexbin": preview_img = not args.nooverlay if not args.nooverlay: t = MockTemplate(axes) fetch = self.bot.fetchers[canvas] preview_img = await render.preview_template(self.bot, t, 1, fetch) process_func = partial(plot.process_histogram, canvas, start, end, axes) x_values, y_values = await self.bot.loop.run_in_executor(None, process_func) plot_func = partial(plot.hexbin_placement_density, ctx, x_values, y_values, args.colormap, args.bins, axes, center, overlay=preview_img) image = await self.bot.loop.run_in_executor(None, plot_func) content = ctx.s("canvas.hexbin_title").format( canvases.pretty_print[canvas], start_str, end_str) elif args.type == "2dhist": preview_img = not args.nooverlay if not args.nooverlay: t = MockTemplate(axes) fetch = self.bot.fetchers[canvas] preview_img = await render.preview_template(self.bot, t, 1, fetch) process_func = partial(plot.process_histogram, canvas, start, end, axes) x_values, y_values = await self.bot.loop.run_in_executor(None, process_func) axes = start_x, end_x, end_y, start_y plot_func = partial(plot.histogram_2d_placement_density, ctx, x_values, y_values, args.colormap, axes, center, overlay=preview_img) image = await self.bot.loop.run_in_executor(None, plot_func) content = ctx.s("canvas.hist2d_title").format( canvases.pretty_print[canvas], start_str, end_str) elif args.type == "online-line": process_func = partial( plot.process_online_line, ctx, canvas, start, end) x_values, y_values = await self.bot.loop.run_in_executor(None, process_func) plot_func = partial( plot.online_line, ctx, x_values, y_values, args.duration, mean=y_values.mean() if args.mean else args.mean) image = await self.bot.loop.run_in_executor(None, plot_func) content = ctx.s("canvas.online_line_title").format( canvases.pretty_print[canvas], start_str, end_str) elif args.type == "placement-hist": process_func = partial(plot.process_placement_hist, ctx, canvas, args.duration) times = await self.bot.loop.run_in_executor(None, process_func) plot_func = partial(plot.placement_hist, ctx, times, args.duration, args.bins) image = await self.bot.loop.run_in_executor(None, plot_func) content = "Histogram of placements on {0} from `{1}` to `{2}`".format( canvases.pretty_print[canvas], start_str, end_str) await ctx.send(content, file=discord.File(image, "stats.png"))
async def alert_stats(self, ctx, *args): try: name = args[0] except IndexError: await ctx.send(ctx.s("error.missing_argument")) return skip = False for arg in args: if any(h == arg for h in ["--help", "-h"]): args = ["--help"] skip = True if not skip: if re.match(r"-\D+", name) is not None: name = args[-1] args = args[:-1] else: args = args[1:] parser = GlimmerArgumentParser(ctx) parser.add_argument("-f", "--faction", default=None, action=FactionAction) parser.add_argument("-d", "--duration", default=DurationAction.get_duration(ctx, "1d"), action=DurationAction) parser.add_argument("-t", "--type", default="comparision", choices=["comparision", "gain"]) try: args = parser.parse_args(args) except TypeError: return log.debug(f"[uuid:{ctx.uuid}] Parsed arguments: {args}") gid = ctx.guild.id if args.faction is not None: gid = args.faction.id template = ctx.session.query(TemplateDb)\ .filter_by(guild_id=gid, name=name).first() if not template: raise TemplateNotFoundError(ctx, gid, name) alert_template = None for t in self.templates: if t.id == template.id: alert_template = t break if not alert_template: await ctx.send( "Error fetching data, is that template an alert template? (See `{0}help alert` for more info)." .format(ctx.prefix)) return start = args.duration.start end = args.duration.end sq = ctx.session.query( Canvas.id).filter_by(nick=template.canvas).subquery() q = ctx.session.query(PixelDb).filter( PixelDb.placed.between(start, end), PixelDb.canvas_id.in_(sq), PixelDb.x.between(template.x, template.x + template.width - 1), PixelDb.y.between(template.y, template.y + template.height - 1)) q = q.order_by(PixelDb.placed) pixels = q.all() if not len(pixels): raise NotEnoughDataError async with ctx.typing(): process_func = partial(self.process, pixels, alert_template, args.duration.days) x_data, y_data = await self.bot.loop.run_in_executor( None, process_func) plot_types = { "comparision": plot.alert_comparision, "gain": plot.alert_gain } plot_func = partial(plot_types.get(args.type), ctx, x_data, y_data[:, 0], y_data[:, 1], args.duration) image = await self.bot.loop.run_in_executor(None, plot_func) if args.type == "comparision": out = ctx.s("alerts.comparision_title").format( template.name, args.duration.days) elif args.type == "gain": out = ctx.s("alerts.gain_title").format( template.name, args.duration.days) await ctx.send(out, file=discord.File(image, "stats.png"))
async def _diff(self, ctx, args, canvas, fetch, palette): """Sends a diff on the image provided. Arguments: ctx - commands.Context object. args - A list of arguments from the user, all strings. canvas - The name of the canvas to look at, string. fetch - The fetch function to use, points to a fetch function from render.py. palette - The palette in use on this canvas, a list of rgb tuples. """ async with ctx.typing(): att = await verify_attachment(ctx) # Order Parsing try: x, y = args[0], args[1] except TypeError: await ctx.send("Error: no arguments were provided.") return if re.match("-\D+", x) != None: x, y = args[-2], args[-1] args = args[:-2] else: args = args[2:] # X and Y Cleanup try: #cleans up x and y by removing all spaces and chars that aren't 0-9 or the minus sign using regex. Then makes em ints x = int(re.sub('[^0-9-]', '', x)) y = int(re.sub('[^0-9-]', '', y)) except ValueError: await ctx.send(ctx.s("canvas.invalid_input")) return # Argument Parsing parser = GlimmerArgumentParser(ctx) parser.add_argument("-e", "--errors", action='store_true') parser.add_argument("-s", "--snapshot", action='store_true') parser.add_argument("-z", "--zoom", type=int, default=1) parser.add_argument("-t", "--excludeTarget", action='store_true') colorFilters = parser.add_mutually_exclusive_group() colorFilters.add_argument("-ec", "--excludeColors", nargs="+", type=int, default=None) colorFilters.add_argument("-oc", "--onlyColors", nargs="+", type=int, default=None) try: a = vars(parser.parse_args(args)) except TypeError: return list_pixels = a["errors"] create_snapshot = a["snapshot"] zoom = a["zoom"] exclude_target = a["excludeTarget"] exclude_colors = a["excludeColors"] only_colors = a["onlyColors"] data = io.BytesIO() await att.save(data) max_zoom = int(math.sqrt(4000000 // (att.width * att.height))) zoom = max(1, min(zoom, max_zoom)) diff_img, tot, err, bad, err_list = await render.diff( x, y, data, zoom, fetch, palette, create_snapshot) done = tot - err perc = done / tot if perc < 0.00005 and done > 0: perc = ">0.00%" elif perc >= 0.99995 and err > 0: perc = "<100.00%" else: perc = "{:.2f}%".format(perc * 100) out = ctx.s("canvas.diff") if bad == 0 else ctx.s( "canvas.diff_bad_color") out = out.format(done, tot, err, perc, bad=bad) with io.BytesIO() as bio: diff_img.save(bio, format="PNG") bio.seek(0) f = discord.File(bio, "diff.png") await ctx.send(content=out, file=f) if list_pixels and len(err_list) > 0: error_list = [] for x, y, current, target in err_list: # Color Filtering c = current if not exclude_target else target if exclude_colors: if c in exclude_colors: continue elif only_colors: if not c in only_colors: continue # The current x,y are in terms of the template area, add to template start coords so they're in terms of canvas x += t.x y += t.y error_list.append(Pixel(current, target, x, y)) checker = Checker(self.bot, ctx, t.canvas, error_list) checker.connect_websocket()