diff options
| -rw-r--r-- | bot/__main__.py | 1 | ||||
| -rw-r--r-- | bot/cogs/free.py | 127 | ||||
| -rw-r--r-- | bot/constants.py | 15 | ||||
| -rw-r--r-- | config-default.yml | 11 |
4 files changed, 154 insertions, 0 deletions
diff --git a/bot/__main__.py b/bot/__main__.py index 3c40a3243..581fa5c8e 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -75,6 +75,7 @@ bot.load_extension("bot.cogs.tags") bot.load_extension("bot.cogs.token_remover") bot.load_extension("bot.cogs.utils") bot.load_extension("bot.cogs.wolfram") +bot.load_extension("bot.cogs.free") if has_rmq: bot.load_extension("bot.cogs.rmq") diff --git a/bot/cogs/free.py b/bot/cogs/free.py new file mode 100644 index 000000000..620449f7e --- /dev/null +++ b/bot/cogs/free.py @@ -0,0 +1,127 @@ +import logging +from datetime import datetime + +from discord import Colour, Embed, Member, utils +from discord.ext import commands +from discord.ext.commands import BucketType, Context, command, cooldown + +from bot.constants import Categories, Free, Roles + + +log = logging.getLogger(__name__) + +TIMEOUT = Free.activity_timeout +RATE = Free.cooldown_rate +PER = Free.cooldown_per + + +class Free: + """Tries to figure out which help channels are free.""" + + PYTHON_HELP_ID = Categories.python_help + + @command(name="free", aliases=('f',)) + @cooldown(RATE, PER, BucketType.channel) + async def free(self, ctx: Context, user: Member = None, seek: int = 2): + """ + Lists free help channels by likeliness of availability. + :param user: accepts user mention, ID, etc. + :param seek: How far back to check the last active message. + + seek is used only when this command is invoked in a help channel. + You cannot override seek without mentioning a user first. + + When seek is 2, we are avoiding considering the last active message + in a channel to be the one that invoked this command. + + When seek is 3 or more, a user has been mentioned on the assumption + that they asked if the channel is free or they asked their question + in an active channel, and we want the message before that happened. + """ + free_channels = [] + python_help = utils.get(ctx.guild.categories, id=self.PYTHON_HELP_ID) + + if user is not None and seek == 2: + seek = 3 + elif not 0 < seek < 10: + seek = 3 + + # Iterate through all the help channels + # to check latest activity + for channel in python_help.channels: + # Seek further back in the help channel + # the command was invoked in + if channel.id == ctx.channel.id: + messages = await channel.history(limit=seek).flatten() + msg = messages[seek-1] + # Otherwise get last message + else: + msg = await channel.history(limit=1).next() # noqa (False positive) + + inactive = (datetime.utcnow() - msg.created_at).seconds + if inactive > TIMEOUT: + free_channels.append((inactive, channel)) + + embed = Embed() + embed.colour = Colour.blurple() + embed.title = "**Looking for a free help channel?**" + + if user is not None: + embed.description = f"**Hey {user.mention}!**\n\n" + else: + embed.description = "" + + # Display all potentially inactive channels + # in descending order of inactivity + if free_channels: + embed.description += "**The following channel{0} look{1} free:**\n\n**".format( + 's' if len(free_channels) > 1 else '', + '' if len(free_channels) > 1 else 's' + ) + + # Sort channels in descending order by seconds + # Get position in list, inactivity, and channel object + # For each channel, add to embed.description + for i, (inactive, channel) in enumerate(sorted(free_channels, reverse=True), 1): + minutes, seconds = divmod(inactive, 60) + if minutes > 59: + hours, minutes = divmod(minutes, 60) + embed.description += f"{i}. {channel.mention} inactive for {hours}h{minutes}m{seconds}s\n\n" + else: + embed.description += f"{i}. {channel.mention} inactive for {minutes}m{seconds}s\n\n" + + embed.description += ("**\nThese channels aren't guaranteed to be free, " + "so use your best judgement and check for yourself.") + else: + embed.description = ("**Doesn't look like any channels are available right now. " + "You're welcome to check for yourself to be sure. " + "If all channels are truly busy, please be patient " + "as one will likely be available soon.**") + + await ctx.send(embed=embed) + + @free.error + async def free_error(self, ctx: Context, error): + """ + If error raised is CommandOnCooldown, and the + user who invoked has the helper role, reset + the cooldown and reinvoke the command. + + Otherwise log the error. + """ + helpers = ctx.guild.get_role(Roles.helpers) + + if isinstance(error, commands.CommandOnCooldown): + if helpers in ctx.author.roles: + # reset cooldown so second invocation + # doesn't bring us back here. + ctx.command.reset_cooldown(ctx) + # return to avoid needlessly logging the error + return await ctx.reinvoke() + + log.exception(error) # Don't ignore other errors + + +def setup(bot): + bot.add_cog(Free()) + log.info("Cog loaded: Free") diff --git a/bot/constants.py b/bot/constants.py index c1375bb13..be713cef2 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -317,6 +317,13 @@ class CleanMessages(metaclass=YAMLGetter): message_limit: int +class Categories(metaclass=YAMLGetter): + section = "guild" + subsection = "categories" + + python_help: int + + class Channels(metaclass=YAMLGetter): section = "guild" subsection = "channels" @@ -466,6 +473,14 @@ class BigBrother(metaclass=YAMLGetter): header_message_limit: int +class Free(metaclass=YAMLGetter): + section = 'free' + + activity_timeout: int + cooldown_rate: int + cooldown_per: float + + # Debug mode DEBUG_MODE = True if 'local' in os.environ.get("SITE_URL", "local") else False diff --git a/config-default.yml b/config-default.yml index 4cc2ff5d5..bb49a46e1 100644 --- a/config-default.yml +++ b/config-default.yml @@ -85,6 +85,9 @@ style: guild: id: 267624335836053506 + categories: + python_help: 356013061213126657 + channels: admins: &ADMINS 365960823622991872 announcements: 354619224620138496 @@ -337,5 +340,13 @@ big_brother: header_message_limit: 15 +free: + # Seconds to elapse for a channel + # to be considered inactive. + activity_timeout: 600 + cooldown_rate: 1 + cooldown_per: 60.0 + + config: required_keys: ['bot.token'] |