Repository: python-discord/bot

Issue:
Role restricted tags
## Abstract
We may want to restrict certain tags to certain roles. For example, we may want tags that are only available to moderators and higher, which explain moderation guidelines, bot commands, stuff like that. We may want tags that are only available to Contributors that explain certain details related to our bot and site repos.

## How do we do that?
Well first of all, **we should probably wait until https://github.com/python-discord/bot/issues/777 is solved.**

Once that has been done, well, I'm not entirely sure how we do it. Off the top of my head, my suggestion would be to simply structure it so that tags restricted to a specific role are put into a subfolder inside the `tags` folder with a name that corresponds with the role. For example, tags inside `/bot/bot/resources/tags/helpers` are available to all Helpers.

Because we have cascading staff roles (every Moderator is also a Helper, every Owner is an Admin, a Moderator and a Helper), we do not necessarily need to support tags that are available to a certain subset of roles - but if you can think of an elegant way to do that, then by all means, let's discuss it.

## Just tell me what to do!
**This is an issue that is intentionally left open-ended.** If you want to handle this, you should be comfortable making changes to the above proposal if, while handling this, you find a major problem with this proposal, or if you come across a better way of solving it. Please discuss this issue with us in #dev-contrib as you work on it, and try to arrive at a good solution.


Discussion:
I would like to work on this.
I think one way to solve this is using Markdown Metadata. There should something like `required_role: moderator` or something and bot should check this.
markdown metadata wouldn't really fit as we will have a problem with segregating the tags. I mean we will have to look into a file to know to whom it is restricted.
@RohanJnr But there is possible to read an MD file on tags getting and cache it.
Nobody is arguing whether it is or isn't possible. The point is that adding metadata for the sake of adding it is unnecessary. If this can be done without having to add our own markdown processing then that's how it should be done.

Resolving patch:
diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py
--- a/bot/cogs/tags.py
+++ b/bot/cogs/tags.py
@@ -4,7 +4,7 @@
 from pathlib import Path
 from typing import Callable, Dict, Iterable, List, Optional
 
-from discord import Colour, Embed
+from discord import Colour, Embed, Member
 from discord.ext.commands import Cog, Context, group
 
 from bot import constants
@@ -35,20 +35,35 @@ def __init__(self, bot: Bot):
     @staticmethod
     def get_tags() -> dict:
         """Get all tags."""
-        # Save all tags in memory.
         cache = {}
-        tag_files = Path("bot", "resources", "tags").iterdir()
-        for file in tag_files:
-            tag_title = file.stem
-            tag = {
-                "title": tag_title,
-                "embed": {
-                    "description": file.read_text(encoding="utf-8")
+
+        base_path = Path("bot", "resources", "tags")
+        for file in base_path.glob("**/*"):
+            if file.is_file():
+                tag_title = file.stem
+                tag = {
+                    "title": tag_title,
+                    "embed": {
+                        "description": file.read_text(),
+                    },
+                    "restricted_to": "developers",
                 }
-            }
-            cache[tag_title] = tag
+
+                # Convert to a list to allow negative indexing.
+                parents = list(file.relative_to(base_path).parents)
+                if len(parents) > 1:
+                    # -1 would be '.' hence -2 is used as the index.
+                    tag["restricted_to"] = parents[-2].name
+
+                cache[tag_title] = tag
+
         return cache
 
+    @staticmethod
+    def check_accessibility(user: Member, tag: dict) -> bool:
+        """Check if user can access a tag."""
+        return tag["restricted_to"].lower() in [role.name.lower() for role in user.roles]
+
     @staticmethod
     def _fuzzy_search(search: str, target: str) -> float:
         """A simple scoring algorithm based on how many letters are found / total, with order in mind."""
@@ -93,7 +108,7 @@ def _get_tag(self, tag_name: str) -> list:
             return self._get_suggestions(tag_name)
         return found
 
-    def _get_tags_via_content(self, check: Callable[[Iterable], bool], keywords: str) -> list:
+    def _get_tags_via_content(self, check: Callable[[Iterable], bool], keywords: str, user: Member) -> list:
         """
         Search for tags via contents.
 
@@ -114,7 +129,8 @@ def _get_tags_via_content(self, check: Callable[[Iterable], bool], keywords: str
 
         matching_tags = []
         for tag in self._cache.values():
-            if check(query in tag['embed']['description'].casefold() for query in keywords_processed):
+            matches = (query in tag['embed']['description'].casefold() for query in keywords_processed)
+            if self.check_accessibility(user, tag) and check(matches):
                 matching_tags.append(tag)
 
         return matching_tags
@@ -152,7 +168,7 @@ async def search_tag_content(self, ctx: Context, *, keywords: str) -> None:
 
         Only search for tags that has ALL the keywords.
         """
-        matching_tags = self._get_tags_via_content(all, keywords)
+        matching_tags = self._get_tags_via_content(all, keywords, ctx.author)
         await self._send_matching_tags(ctx, keywords, matching_tags)
 
     @search_tag_content.command(name='any')
@@ -162,7 +178,7 @@ async def search_tag_content_any_keyword(self, ctx: Context, *, keywords: Option
 
         Search for tags that has ANY of the keywords.
         """
-        matching_tags = self._get_tags_via_content(any, keywords or 'any')
+        matching_tags = self._get_tags_via_content(any, keywords or 'any', ctx.author)
         await self._send_matching_tags(ctx, keywords, matching_tags)
 
     @tags_group.command(name='get', aliases=('show', 'g'))
@@ -198,7 +214,13 @@ def _command_on_cooldown(tag_name: str) -> bool:
             return
 
         if tag_name is not None:
-            founds = self._get_tag(tag_name)
+            temp_founds = self._get_tag(tag_name)
+
+            founds = []
+
+            for found_tag in temp_founds:
+                if self.check_accessibility(ctx.author, found_tag):
+                    founds.append(found_tag)
 
             if len(founds) == 1:
                 tag = founds[0]
@@ -237,7 +259,10 @@ def _command_on_cooldown(tag_name: str) -> bool:
             else:
                 embed: Embed = Embed(title="**Current tags**")
                 await LinePaginator.paginate(
-                    sorted(f"**»**   {tag['title']}" for tag in tags),
+                    sorted(
+                        f"**»**   {tag['title']}" for tag in tags
+                        if self.check_accessibility(ctx.author, tag)
+                    ),
                     ctx,
                     embed,
                     footer_text=FOOTER_TEXT,


Test patch: