class AdvanceWars(Cog): """ Commands for working with Advance Wars maps. Will currently support AWS map files and AWBW maps, both text, file, and links. Other features soon to come™ Maps can be automatically loaded from AWS, CSV, and AWBW links if `Map Listen` is turned on. See `[p]help map listen` for more details. """ def __init__(self, bot: Bot): self.bot = bot self.config = SubRedis(bot.db, "maps") self.listen_for_maps = bool(self.config.get("listen_for_maps")) or False self.buffer_channel = self.bot.get_channel(id=434551085185630218) self.loaded_maps = {} """ ################################# # General use commands for maps # ################################# """ @group(name="map", invoke_without_command=True, usage="[map file or link]") async def _map(self, ctx: Context, arg: str = "", *, args: str = ""): """The base command for working with maps This command will retrieve map information on a loaded map. This command will load a map if you do not already have one loaded. See `[p]help map load` for more information on loading maps.""" if ctx.author.id in self.loaded_maps.keys(): # Subcommands available to ctx.author avail_cmds = [] for cmd in ctx.command.walk_commands(): if await cmd.can_run(ctx): avail_cmds.append(cmd.name) if arg in avail_cmds: ctx.message.content = f"{self._inv(ctx)} {arg} {args}" ctx = await self.bot.get_context(ctx.message) await self.bot.invoke(ctx) else: ctx.message.content = f"{self._inv(ctx)} info {arg} {args}" ctx = await self.bot.get_context(ctx.message) await self.bot.invoke(ctx) else: ctx.message.content = f"{self._inv(ctx)} load {arg} {args}" ctx = await self.bot.get_context(ctx.message) await self.bot.invoke(ctx) @_map.command(name="load", usage="[title]") async def _load(self, ctx: Context, title: str = None, *, _: str = ""): """Load a map to be worked with This command will load a map to be worked with. You can load maps through 5 different methods: __AWS File__ `[p]map load` with attached AWS file __AWBW Map CSV File__ `[p]map load` with attached CSV or TXT file __AWBW Map Link__ `[p]map load http://awbw.amarriner.com/prevmaps.php?maps_id=12345` __AWBW Map ID__ `[p]map load 12345` __AWBW Text Style Map__ `[p]map load "Example Map"` ``` 1,2,3,4,5 6,7,8,9,10 11,12,13,14,15 16,17,18,19,20 21,22,23,24,25 ``` For text maps, the first line must be the title. For multi-word titles, you must wrap the title in quotes (" " or ' '). You will receive an error if not all rows have the same number of columns. Markdown boxes are not necessary, but may make typing text maps easier. Text maps stay loaded as long as you're working with them with a time out of 5 minutes. After 5 minutes with no activity, maps are automatically unloaded and must loaded again to continue working with them.""" awmap = await CheckMap.check(ctx.message, title, verify=True) if not awmap: raise InvalidMapError await self.em_load(ctx.channel, awmap) await self.timed_store(ctx.author, awmap) @_map.group(name="draw", invoke_without_command=False, aliases=["mod"]) async def draw(self, ctx: Context): pass @draw.command(name="terr", aliases=["terrain"]) async def terr(self, ctx: Context, terr: Union[str, int], ctry: Union[str, int], *, coords): """Modifies the terrain contents of a list of tiles """ awmap = self.get_loaded_map(ctx.author) if not awmap: raise NoLoadedMapError coords = [list(coords.split(" ")[i:2 + i]) for i in range(0, len(coords.split(" ")), 2)] terr, ctry = int(terr), int(ctry) for coord in coords: x, y = coord x, y = int(x), int(y) awmap.mod_terr(x, y, terr, ctry) await self.timed_store(ctx.author, awmap) await self.em_load(ctx.channel, awmap) @_map.command(name="download", usage=" ") async def download(self, ctx: Context, *, _: str = ""): """Download your currently loaded map Use this command when you have a map loaded with `[p]map load` to get download links for all supported map formats. Currently supported formats: `Advance Wars Series Map Editor File (*.aws)` `Advance Wars By Web Text Map File (*.csv)`""" awmap = self.get_loaded_map(ctx.author) if not awmap: raise NoLoadedMapError await self.em_download(ctx.channel, awmap) @_map.command(name="info", usage=" ", enabled=False) async def info(self, ctx: Context, *, _: str = ""): """Something something information about a map""" raise UnimplementedError """ ########################## # Some Utility Functions # ########################## """ async def timed_store(self, user: Member, awmap: AWMap) -> None: """ Stores an AWMap by user ID with a expiry of 5 minutes. Loading another map will reset the timer. Stored in `self.loaded_maps` dict with structure: user.id: { "awmap": awmap, "ts": datetime.utcnow() } :param user: `discord.Member` instance of command author :param awmap: `AWMap` instance of map loaded by `user` :returns: `None` (Does not return) """ ts = datetime.utcnow() self.loaded_maps[user.id] = { "ts": ts, "awmap": awmap } await sleep(300) if self.loaded_maps[user.id]["ts"] == ts: self.loaded_maps.pop(user.id) def get_loaded_map(self, user: Member) -> Union[AWMap, None]: """Will retrieve loaded map object for a given user. :param user: `discord.Member` instance for user :return: `AWMap` object or `None` if no loaded map""" store = self.loaded_maps.get(user.id) if store: return store["awmap"] else: return None async def get_hosted_file(self, file: File) -> str: """Sends a message to Discord containing a file to return the file URL hosted on Discord :param file: `discord.File` object containing the file to host :return: `str` URL of hosted file""" msg = await self.buffer_channel.send(file=file) return msg.attachments[0].url async def get_aws(self, awmap: AWMap) -> str: """Uses `AWMap`'s `to_aws` parameter to export a map as AWS file then returns a link to the file hosted on Discord using `get_hosted_file` method :param awmap: `AWMap` instance of map to export :return: `str` URL of hosted AWS file""" if awmap.title: title = awmap.title else: title = "Untitled" attachment = File( fp=BytesIO(awmap.to_aws), filename=f"{title}.aws" ) url = await self.get_hosted_file(attachment) return url async def get_awbw(self, awmap: AWMap) -> str: """Uses `AWMap`'s `to_awbw` parameter to export a map to an AWBW CSV text file then returns a link to the file hosted on Discord using `get_hosted_file` method :param awmap: `AWMap` instance of map to export :return: `str` URL of hosted CSV file""" if awmap.title: title = awmap.title else: title = "Untitled" attachment = File( fp=BytesIO(awmap.to_awbw.encode("utf-8")), filename=f"{title}.csv" ) url = await self.get_hosted_file(attachment) return url async def get_minimap(self, awmap: AWMap) -> str: """Uses `AWMap`'s `minimap` parameter to generate a `PIL` image of a minimap representing the loaded map then returns a link to the file hosted on Discord using the `get_hosted_file` method :param awmap: `AWMap` instance of map :return: `str` URL of hosted minimap image""" if awmap.title: title = awmap.title else: title = "[Untitled]" attachment = File(fp=awmap.minimap, filename=f"{title}.gif") url = await self.get_hosted_file(attachment) return url """ ###################################### # Some Special Utilities for Discord # ###################################### """ @staticmethod def _color(channel) -> int: if isinstance(channel, DMChannel): return 0 else: return channel.guild.me.colour.value @staticmethod def _inv(ctx: Context) -> str: return f"{ctx.prefix}{ctx.invoked_with}" """ ##################### # Formatting Embeds # ##################### """ def base_embed(self, channel, awmap: AWMap) -> Embed: """Formats and returns the base embed with map title and author.""" if awmap.awbw_id: a_url = f"http://awbw.amarriner.com/profile.php?username={quote(awmap.author)}" if awmap.author == "[Unknown]": author = "Design Map by [Unknown]" else: author = f"Design map by [{awmap.author}]({a_url})" m_url = f"http://awbw.amarriner.com/prevmaps.php?maps_id={awmap.awbw_id}" else: author = f"Design map by {awmap.author}" m_url = "" return Embed(color=self._color(channel), title=awmap.title, description=author, url=m_url) async def em_load(self, channel, awmap: AWMap): """Formats and sends an embed to `channel` appropriate for when a map is loaded for a user.""" em = self.base_embed(channel, awmap) image_url = await self.get_minimap(awmap) em.set_image(url=image_url) return await channel.send(embed=em) async def em_download(self, channel, awmap: AWMap): """Formats and sends an embed to `channel` containing downloads for the supported map types.""" em = self.base_embed(channel, awmap) aws = await self.get_aws(awmap) csv = await self.get_awbw(awmap) thumb = await self.get_minimap(awmap) em.add_field(name="Downloads", value=f"[AWS]({aws})\n[AWBW CSV]({csv})") em.set_thumbnail(url=thumb) return await channel.send(embed=em) """ ############################################### # Administrative Commands for AdvanceWars cog # ############################################### """ @sudo() @_map.command(name="listen", hidden=True, usage="[on / off]") async def listen(self, ctx: Context, *, arg: str = ""): """Toggle active listening for maps With this option turned on, all messages will be checked for valid maps. Messages containing valid maps will be treated as a `[p]map load` command.""" if arg.strip(" ").lower() in "on yes true y t 1".split(" "): self.config.set("listen_for_maps", "True") self.listen_for_maps = True elif arg.strip(" ").lower() in "off no false n f 0".split(" "): self.config.set("listen_for_maps", "False") self.listen_for_maps = False em = Embed(color=self._color(ctx.channel), title="BattleMaps Config", description=f"Active Listen For Maps: `{self.listen_for_maps}`") await ctx.send(embed=em) @sudo() @_map.command(name="viewallmaps", hidden=True, aliases=["vam"]) async def viewallmaps(self, ctx: Context): """View all currently loaded maps. Administrative command that will display Map titles and user IDs for all currently loaded maps""" em = Embed(color=self._color(ctx.channel), title="All Currently Loaded Maps", description="\n".join(f"{k} @ {v['ts']}: {v['awmap'].title}" for k, v in self.loaded_maps.items())) await ctx.send(embed=em) # @checks.sudo() # @_map.command(name="pull", hidden=True, aliases=["update"], enabled=False) # async def _pull(self, ctx: Context): # """Update the converter core # # Uses `pull` method to update AWSMapConvert # and reload the cog.""" # if self.pull(): # await ctx.send("Converter core updated.\nReloading cog.") # self.bot.remove_cog("AdvanceWars") # self.bot.add_cog(AdvanceWars(self.bot)) # else: # await ctx.send("Converter core already up-to-date.") # @staticmethod # def pull() -> bool: # """Git Pull the AWSMapConverter. If it is not # found, will Git Clone # # :returns bool: `True` if converter was updated # successfully. `False` otherwise""" # # cmd = "git -C AWSMapConverter --no-pager pull".split(" ") # params = { # "universal_newlines": True, # "cwd": os.getcwd(), # "stdout": subprocess.PIPE, # "stderr": subprocess.STDOUT # } # # ret = subprocess.run(cmd, **params) # if "up-to-date" in ret.stdout: # return False # elif "fatal: Not a git repository" in ret.stdout: # cmd = "git clone --no-pager" \ # "https://github.com/SirThane/AWSMapConverter.git".split(" ") # subprocess.run(cmd, **params) # return True # else: # return True @Cog.listener("on_message") async def on_message(self, msg: Message): if self.listen_for_maps and not msg.author.bot: if not any([msg.content.startswith(prefix) for prefix in self.bot.command_prefix(self.bot, msg)]): awmap = await CheckMap.check(msg, skips=["msg_csv", "id"]) if awmap: await self.em_load(msg.channel, awmap) await self.timed_store(msg.author, awmap)
class GitHub(Cog): """GitHub""" def __init__(self, bot: Bot): self.bot = bot self.config = SubRedis(bot.db, "github") self.errorlog = bot.errorlog self.gh_client = self.try_auth() if self.gh_client: self._user = self.user = self.gh_client.get_user() self._repo = self.repo = self.user.get_repo(self.bot.APP_NAME) def cog_check(self, ctx: Context) -> bool: return ctx. def try_auth(self) -> Optional[Github]: """Create Github client object and attempt hitting API with token""" token = self.config.get("token") if token: try: gh_client = Github(token) _ = gh_client.get_rate_limit() return gh_client except BadCredentialsException: return None @group(name="gh", invoke_without_command=True) @is_authed async def gh(self, ctx: Context): """GitHub Something""" user = f"[{self.user.name}]({self.user.html_url})" if self.user else "None Selected" repo = f"[{self.repo.name}]({self.repo.html_url})" if self.repo else "None Selected" em = Embed( title="GitHub", description=f"**__Current Selections:__**\n" f"__User:__ {user}\n" f"__Repository:__ {repo}", color=Colour.green() ) await ctx.send(embed=em) @gh.command(name="auth") async def auth(self, ctx: Context): """Authenticate With GitHub Token Something something""" pass @gh.command(name="user") async def user(self, ctx: Context, *, user: str = None): """User Selects a GitHub user account and shows a brief of the profile. `[p]gh user <username>` will select a user account `[p]gh user self` will select your user account `[p]gh user` will display the currently selected user""" try: if user == "self": self.user = user = self.gh_client.get_user() elif user: self.user = user = self.gh_client.get_user(user) else: if self.user: user = self.user else: return await self.bot.send_help_for( ctx.command, "No account is currently selected." ) if self.repo.owner.name != self.user.name: self.repo = None repos = len(list(user.get_repos())) gists = len(list(user.get_gists())) stars = len(list(user.get_gists())) em = Embed( title=f"{user.login}'s Public GitHub Profile", description=f"*\"{user.bio}\"*\n\n" f"{Emoji.repo} [Repositories]({user.html_url}?tab=repositories): {repos}\n" f"{Emoji.gist} [Gists](https://gist.github.com/{user.login}): {gists}\n" f"{Emoji.star} [Starred Repositories]({user.html_url}?tab=stars): {stars}", color=Colour.green() ) em.set_author(name=user.name, url=user.html_url, icon_url=user.avatar_url) except: em = Embed( title="GitHub: Error", description="Unable to load user or user not found", color=Colour.red() ) await ctx.send(embed=em) @gh.command(name="repo") async def repo(self, ctx: Context, *, name: str = None): pass @gh.group(name="issues", invoke_without_command=True, aliases=["issue"]) async def issues(self, ctx: Context, state: str = "all"): """Issues View and manage issues on a repo""" state = state.lower() if state not in ("open", "closed", "all"): return await self.bot.send_help_for(ctx.command, "Valid states: `open`, `closed`, `all`") em = Embed() for issue in self.repo.get_issues(state=state): pass