Repository: python-discord/bot

Issue:
Source command.
A source command has been discussed as a good addition in the past for the bot, with the following features:
- `!source` shows the base github repository link
- `!source <command>` will provide a github link to the relevant module:line_number directly

Feel free to recommend any additional suggestions in the comments below.


Discussion:
Not sure if someone else brought this command up, but I did mention to lemon that I was interested in writing this (However if someone wants to beat me to it, that's ok).

I got the idea from this: [RoboDanny](https://github.com/Rapptz/RoboDanny/blob/04e401873221fed4de1b58cbdffd62471b29f675/cogs/meta.py#L328-L366)

I've already started to test out the same method used above, but it doesn't work perfectly with our bot. Because of some redirection decorators and such, `bot.get_command` gets confused and returns code from the decorator instead of the cog being decorated. More testing is needed.
Nobody has reserved it so you're welcome to keep working on it if you want, just hold up on a PR until we have moved to Django and tested it on it. 

Sounds like you've got a good head start :)
@fiskenslakt I think it's fine for you to assign yourself to this issue.
@scragly right yea, was definitely gonna wait till after the feature freeze.

@MarkKoz I don't think I *can* assign myself the issue lol
Strange. I can assign myself and a bunch of others, but your name doesn't show up there.
Good enough. It's likely because of access levels and that he hasn't contributed to this repository yet.
Maybe you need to be on the contributors team? Not sure
@scragly Does my PR for the `!free` command not count as a contribution to this repo?
That's on bot. Oh wait, this is bot!
Yes, it's a repo setting, we'll shove you in the Contributors team haha. You haven't been added yet.
gonna work on this

btw can i have some more details on this, what can the commands be?
and also an example?


No progress in 4-5 months; unassigning. This is now open for anyone to work on.

Resolving patch:
diff --git a/bot/__main__.py b/bot/__main__.py
--- a/bot/__main__.py
+++ b/bot/__main__.py
@@ -66,6 +66,7 @@
 bot.load_extension("bot.cogs.reminders")
 bot.load_extension("bot.cogs.site")
 bot.load_extension("bot.cogs.snekbox")
+bot.load_extension("bot.cogs.source")
 bot.load_extension("bot.cogs.stats")
 bot.load_extension("bot.cogs.sync")
 bot.load_extension("bot.cogs.tags")
diff --git a/bot/cogs/source.py b/bot/cogs/source.py
new file mode 100644
--- /dev/null
+++ b/bot/cogs/source.py
@@ -0,0 +1,133 @@
+import inspect
+from pathlib import Path
+from typing import Optional, Tuple, Union
+
+from discord import Embed
+from discord.ext import commands
+
+from bot.bot import Bot
+from bot.constants import URLs
+
+SourceType = Union[commands.HelpCommand, commands.Command, commands.Cog, str, commands.ExtensionNotLoaded]
+
+
+class SourceConverter(commands.Converter):
+    """Convert an argument into a help command, tag, command, or cog."""
+
+    async def convert(self, ctx: commands.Context, argument: str) -> SourceType:
+        """Convert argument into source object."""
+        if argument.lower().startswith("help"):
+            return ctx.bot.help_command
+
+        cog = ctx.bot.get_cog(argument)
+        if cog:
+            return cog
+
+        cmd = ctx.bot.get_command(argument)
+        if cmd:
+            return cmd
+
+        tags_cog = ctx.bot.get_cog("Tags")
+        show_tag = True
+
+        if not tags_cog:
+            show_tag = False
+        elif argument.lower() in tags_cog._cache:
+            return argument.lower()
+
+        raise commands.BadArgument(
+            f"Unable to convert `{argument}` to valid command{', tag,' if show_tag else ''} or Cog."
+        )
+
+
+class BotSource(commands.Cog):
+    """Displays information about the bot's source code."""
+
+    def __init__(self, bot: Bot):
+        self.bot = bot
+
+    @commands.command(name="source", aliases=("src",))
+    async def source_command(self, ctx: commands.Context, *, source_item: SourceConverter = None) -> None:
+        """Display information and a GitHub link to the source code of a command, tag, or cog."""
+        if not source_item:
+            embed = Embed(title="Bot's GitHub Repository")
+            embed.add_field(name="Repository", value=f"[Go to GitHub]({URLs.github_bot_repo})")
+            embed.set_thumbnail(url="https://avatars1.githubusercontent.com/u/9919")
+            await ctx.send(embed=embed)
+            return
+
+        embed = await self.build_embed(source_item)
+        await ctx.send(embed=embed)
+
+    def get_source_link(self, source_item: SourceType) -> Tuple[str, str, Optional[int]]:
+        """Build GitHub link of source item, return this link, file location and first line number."""
+        if isinstance(source_item, commands.HelpCommand):
+            src = type(source_item)
+            filename = inspect.getsourcefile(src)
+        elif isinstance(source_item, commands.Command):
+            if source_item.cog_name == "Alias":
+                cmd_name = source_item.callback.__name__.replace("_alias", "")
+                cmd = self.bot.get_command(cmd_name.replace("_", " "))
+                src = cmd.callback.__code__
+                filename = src.co_filename
+            else:
+                src = source_item.callback.__code__
+                filename = src.co_filename
+        elif isinstance(source_item, str):
+            tags_cog = self.bot.get_cog("Tags")
+            filename = tags_cog._cache[source_item]["location"]
+        else:
+            src = type(source_item)
+            filename = inspect.getsourcefile(src)
+
+        if not isinstance(source_item, str):
+            lines, first_line_no = inspect.getsourcelines(src)
+            lines_extension = f"#L{first_line_no}-L{first_line_no+len(lines)-1}"
+        else:
+            first_line_no = None
+            lines_extension = ""
+
+        # Handle tag file location differently than others to avoid errors in some cases
+        if not first_line_no:
+            file_location = Path(filename).relative_to("/bot/")
+        else:
+            file_location = Path(filename).relative_to(Path.cwd()).as_posix()
+
+        url = f"{URLs.github_bot_repo}/blob/master/{file_location}{lines_extension}"
+
+        return url, file_location, first_line_no or None
+
+    async def build_embed(self, source_object: SourceType) -> Optional[Embed]:
+        """Build embed based on source object."""
+        url, location, first_line = self.get_source_link(source_object)
+
+        if isinstance(source_object, commands.HelpCommand):
+            title = "Help Command"
+            description = source_object.__doc__.splitlines()[1]
+        elif isinstance(source_object, commands.Command):
+            if source_object.cog_name == "Alias":
+                cmd_name = source_object.callback.__name__.replace("_alias", "")
+                cmd = self.bot.get_command(cmd_name.replace("_", " "))
+                description = cmd.short_doc
+            else:
+                description = source_object.short_doc
+
+            title = f"Command: {source_object.qualified_name}"
+        elif isinstance(source_object, str):
+            title = f"Tag: {source_object}"
+            description = ""
+        else:
+            title = f"Cog: {source_object.qualified_name}"
+            description = source_object.description.splitlines()[0]
+
+        embed = Embed(title=title, description=description)
+        embed.add_field(name="Source Code", value=f"[Go to GitHub]({url})")
+        line_text = f":{first_line}" if first_line else ""
+        embed.set_footer(text=f"{location}{line_text}")
+
+        return embed
+
+
+def setup(bot: Bot) -> None:
+    """Load the BotSource cog."""
+    bot.add_cog(BotSource(bot))
diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py
--- a/bot/cogs/tags.py
+++ b/bot/cogs/tags.py
@@ -47,6 +47,7 @@ def get_tags() -> dict:
                         "description": file.read_text(encoding="utf8"),
                     },
                     "restricted_to": "developers",
+                    "location": f"/bot/{file}"
                 }
 
                 # Convert to a list to allow negative indexing.


Test patch: