diff options
| author | 2018-03-02 23:32:10 +0100 | |
|---|---|---|
| committer | 2018-03-02 23:32:10 +0100 | |
| commit | 909b1edf8d6b7d6c72731280b9db1e781d31fad3 (patch) | |
| tree | fc6f0c1ba784770d17e8adbfe4456e6dda4a2e6e | |
| parent | adds vagrant file (#14) (diff) | |
Tag feature (#21)
* [WIP] get_tag()
* [WIP] get_tag()
* [WIP] get_tag()
* [WIP] - tags
* [WIP] renamed to docs, tested and verified get and set commands, still need to add cooldowns and delete.
* [WIP] get_tag()
* [WIP] get_tag()
* [WIP] get_tag()
* [WIP] - tags
* [WIP] renamed to docs, tested and verified get and set commands, still need to add cooldowns and delete.
* removed the tag.py file
* Refactoring docs to tags
* [WIP] polish for the tags bot feature
* Added pagination, lots of output polish, input sanitization
* seperated out the the API interaction so it can be accessed from other cogs. Added a ton of polish, better documentation, and a help function if you just type bot.tags(). The core functionality should be done now, just missing the cooldown decorator.
* Cooldown system per command per channel. Some annotations. Ready for PR!
* oops, had left local url in.
* removing some out of scope stuff
* Using API key from constants
* Replacing Union[str, None] with Optional[str]
* adding some namespaceless commands
* inverting some conditionals with returns in them.
* removing some mysterious commas, and adding better error messages if the database operation fails
* adhering to the standard set in clickup cog to prevent shadowing.
* alphabetized cog load block, moved headers into a class attribute, and fixed punctuation consistency issues.
Diffstat (limited to '')
| -rw-r--r-- | bot/__main__.py | 1 | ||||
| -rw-r--r-- | bot/cogs/tags.py | 263 | ||||
| -rw-r--r-- | bot/constants.py | 5 |
3 files changed, 268 insertions, 1 deletions
diff --git a/bot/__main__.py b/bot/__main__.py index 895603689..88991ac53 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -34,6 +34,7 @@ bot.load_extension("bot.cogs.clickup") bot.load_extension("bot.cogs.deployment") bot.load_extension("bot.cogs.eval") bot.load_extension("bot.cogs.fun") +bot.load_extension("bot.cogs.tags") bot.load_extension("bot.cogs.verification") bot.run(os.environ.get("BOT_TOKEN")) diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py new file mode 100644 index 000000000..2fe5f72ef --- /dev/null +++ b/bot/cogs/tags.py @@ -0,0 +1,263 @@ +import time +from typing import Optional + +from aiohttp import ClientSession + +from discord import Colour, Embed +from discord.ext.commands import AutoShardedBot, Context, command + +from bot.constants import ADMIN_ROLE, MODERATOR_ROLE, OWNER_ROLE +from bot.constants import SITE_API_KEY, SITE_API_TAGS_URL, TAG_COOLDOWN +from bot.decorators import with_role +from bot.utils import paginate + + +class Tags: + """ + Save new tags and fetch existing tags. + """ + + def __init__(self, bot: AutoShardedBot): + self.bot = bot + self.tag_cooldowns = {} + self.headers = {"X-API-KEY": SITE_API_KEY} + + async def get_tag_data(self, tag_name: Optional[str] = None) -> dict: + """ + Retrieve the tag_data from our API + + :param tag_name: the tag to retrieve + :return: + if tag_name was provided, returns a dict with tag data. + if not, returns a list of dicts with all tag data. + + """ + params = {} + + if tag_name: + params['tag_name'] = tag_name + + async with ClientSession() as session: + response = await session.get(SITE_API_TAGS_URL, headers=self.headers, params=params) + tag_data = await response.json() + + return tag_data + + async def post_tag_data(self, tag_name: str, tag_content: str) -> dict: + """ + Send some tag_data to our API to have it saved in the database. + + :param tag_name: The name of the tag to create or edit. + :param tag_content: The content of the tag. + :return: json response from the API in the following format: + { + 'success': bool + } + """ + + params = { + 'tag_name': tag_name, + 'tag_content': tag_content + } + + async with ClientSession() as session: + response = await session.post(SITE_API_TAGS_URL, headers=self.headers, json=params) + tag_data = await response.json() + + return tag_data + + async def delete_tag_data(self, tag_name: str) -> dict: + """ + Delete a tag using our API. + + :param tag_name: The name of the tag to delete. + :return: json response from the API in the following format: + { + 'success': bool + } + """ + + params = {} + + if tag_name: + params['tag_name'] = tag_name + + async with ClientSession() as session: + response = await session.delete(SITE_API_TAGS_URL, headers=self.headers, json=params) + tag_data = await response.json() + + return tag_data + + @command(name="tags()", aliases=["tags"], hidden=True) + async def info_command(self, ctx: Context): + """ + Show available methods for this class. + + :param ctx: Discord message context + """ + + return await ctx.invoke(self.bot.get_command("help"), "Tags") + + @command(name="tags.get()", aliases=["tags.get", "tags.show()", "tags.show", "get_tag"]) + async def get_command(self, ctx: Context, tag_name: Optional[str] = None): + """ + Get a list of all tags or a specified tag. + + :param ctx: Discord message context + :param tag_name: + If provided, this function shows data for that specific tag. + If not provided, this function shows the caller a list of all tags. + """ + + def _command_on_cooldown(tag_name: str) -> bool: + """ + Check if the command is currently on cooldown. + The cooldown duration is set in constants.py. + + This works on a per-tag, per-channel basis. + :param tag_name: The name of the command to check. + :return: True if the command is cooling down. Otherwise False. + """ + + now = time.time() + + cooldown_conditions = ( + tag_name + and tag_name in self.tag_cooldowns + and (now - self.tag_cooldowns[tag_name]["time"]) < TAG_COOLDOWN + and self.tag_cooldowns[tag_name]["channel"] == ctx.channel.id + ) + + if cooldown_conditions: + return True + return False + + if _command_on_cooldown(tag_name): + time_left = TAG_COOLDOWN - (time.time() - self.tag_cooldowns[tag_name]["time"]) + print(f"That command is currently on cooldown. Try again in {time_left:.1f} seconds.") + return + + embed = Embed() + tags = [] + + tag_data = await self.get_tag_data(tag_name) + + # If we found something, prepare that data + if tag_data: + embed.colour = Colour.blurple() + + if tag_name: + embed.title = tag_name + self.tag_cooldowns[tag_name] = { + "time": time.time(), + "channel": ctx.channel.id + } + + else: + embed.title = "**Current tags**" + + if isinstance(tag_data, list): + tags = [f"**»** {tag['tag_name']}" for tag in tag_data] + tags = sorted(tags) + + else: + embed.description = tag_data['tag_content'] + + # If not, prepare an error message. + else: + embed.colour = Colour.red() + embed.description = "**There are no tags in the database!**" + + if isinstance(tag_data, dict): + embed.description = f"Unknown tag: **{tag_name}**" + + if tag_name: + embed.set_footer(text="To show a list of all tags, use bot.tags.get().") + embed.title = "Tag not found" + + # Paginate if this is a list of all tags + if tags: + return await paginate( + (lines for lines in tags), + ctx, embed, + footer_text="To show a tag, type bot.tags.get <tagname>.", + empty=False, + max_size=200 + ) + + return await ctx.send(embed=embed) + + @with_role(ADMIN_ROLE, OWNER_ROLE, MODERATOR_ROLE) + @command(name="tags.set()", aliases=["tags.set", "tags.add", "tags.add()", "tags.edit", "tags.edit()", "add_tag"]) + async def set_command(self, ctx: Context, tag_name: str, tag_content: str): + """ + Create a new tag or edit an existing one. + + :param ctx: discord message context + :param tag_name: The name of the tag to create or edit. + :param tag_content: The content of the tag. + """ + + embed = Embed() + embed.colour = Colour.red() + + if "\n" in tag_name: + embed.title = "Please don't do that" + embed.description = "Don't be ridiculous. Newlines are obviously not allowed in the tag name." + + else: + if not (tag_name and tag_content): + embed.title = "Missing parameters" + embed.description = "The tag needs both a name and some content." + return await ctx.send(embed=embed) + + tag_name = tag_name.lower() + tag_data = await self.post_tag_data(tag_name, tag_content) + + if tag_data.get("success"): + embed.colour = Colour.blurple() + embed.title = "Tag successfully added" + embed.description = f"**{tag_name}** added to tag database." + else: + embed.title = "Database error", + embed.description = ("There was a problem adding the data to the tags database. " + "Please try again. If the problem persists, check the API logs.") + + return await ctx.send(embed=embed) + + @with_role(ADMIN_ROLE, OWNER_ROLE) + @command(name="tags.delete()", aliases=["tags.delete", "tags.remove", "tags.remove()", "remove_tag"]) + async def delete_command(self, ctx: Context, tag_name: str): + """ + Remove a tag from the database. + + :param ctx: discord message context + :param tag_name: The name of the tag to delete. + """ + + embed = Embed() + embed.colour = Colour.red() + + if not tag_name: + embed.title = "Missing parameters" + embed.description = "This method requires a `tag_name` parameter." + return await ctx.send(embed=embed) + + tag_data = await self.delete_tag_data(tag_name) + + if tag_data.get("success"): + embed.colour = Colour.blurple() + embed.title = tag_name + embed.description = f"Tag successfully removed: {tag_name}." + + else: + embed.title = "Database error", + embed.description = ("There was a problem deleting the data from the tags database. " + "Please try again. If the problem persists, check the API logs.") + + return await ctx.send(embed=embed) + + +def setup(bot): + bot.add_cog(Tags(bot)) + print("Cog loaded: Tags") diff --git a/bot/constants.py b/bot/constants.py index cf42996c1..bf6f1ce2e 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -22,6 +22,8 @@ CLICKUP_KEY = os.environ.get("CLICKUP_KEY") CLICKUP_SPACE = 757069 CLICKUP_TEAM = 754996 +TAG_COOLDOWN = 60 # Per channel, per tag + GITHUB_URL_BOT = "https://github.com/discord-python/bot" BOT_AVATAR_URL = "https://raw.githubusercontent.com/discord-python/branding/master/logos/logo_circle.png" @@ -32,7 +34,8 @@ DEPLOY_BOT_KEY = os.environ.get("DEPLOY_BOT_KEY") DEPLOY_SITE_KEY = os.environ.get("DEPLOY_SITE_KEY") SITE_API_KEY = os.environ.get("BOT_API_KEY") -SITE_API_USER_URL = "https://api.pythondiscord.com/user" +SITE_API_USER_URL = "http://api.pythondiscord.com/user" +SITE_API_TAGS_URL = "http://api.pythondiscord.com/tags" HELP_PREFIX = "bot." |