aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore5
-rw-r--r--bot/__main__.py5
-rw-r--r--bot/bot.py9
-rw-r--r--bot/constants.py62
-rw-r--r--bot/decorators.py93
-rw-r--r--bot/resources/evergreen/speedrun_links.json18
-rw-r--r--bot/seasons/christmas/adventofcode.py2
-rw-r--r--bot/seasons/easter/egg_hunt/cog.py3
-rw-r--r--bot/seasons/easter/egg_hunt/constants.py3
-rw-r--r--bot/seasons/evergreen/error_handler.py15
-rw-r--r--bot/seasons/evergreen/issues.py2
-rw-r--r--bot/seasons/evergreen/speedrun.py28
-rw-r--r--bot/seasons/season.py3
13 files changed, 216 insertions, 32 deletions
diff --git a/.gitignore b/.gitignore
index 8f848483..8a21b668 100644
--- a/.gitignore
+++ b/.gitignore
@@ -111,4 +111,7 @@ venv.bak/
# jetbrains
.idea/
-.DS_Store \ No newline at end of file
+.DS_Store
+
+# vscode
+.vscode/
diff --git a/bot/__main__.py b/bot/__main__.py
index a3b68ec1..9dc0b173 100644
--- a/bot/__main__.py
+++ b/bot/__main__.py
@@ -1,8 +1,11 @@
import logging
-from bot.constants import Client, bot
+from bot.bot import bot
+from bot.constants import Client, STAFF_ROLES, WHITELISTED_CHANNELS
+from bot.decorators import in_channel_check
log = logging.getLogger(__name__)
+bot.add_check(in_channel_check(*WHITELISTED_CHANNELS, bypass_roles=STAFF_ROLES))
bot.load_extension("bot.seasons")
bot.run(Client.token)
diff --git a/bot/bot.py b/bot/bot.py
index 24e919f2..86028838 100644
--- a/bot/bot.py
+++ b/bot/bot.py
@@ -7,11 +7,11 @@ from aiohttp import AsyncResolver, ClientSession, TCPConnector
from discord import Embed
from discord.ext import commands
-from bot import constants
+from bot.constants import Channels, Client
log = logging.getLogger(__name__)
-__all__ = ('SeasonalBot',)
+__all__ = ('SeasonalBot', 'bot')
class SeasonalBot(commands.Bot):
@@ -42,7 +42,7 @@ class SeasonalBot(commands.Bot):
async def send_log(self, title: str, details: str = None, *, icon: str = None):
"""Send an embed message to the devlog channel."""
- devlog = self.get_channel(constants.Channels.devlog)
+ devlog = self.get_channel(Channels.devlog)
if not devlog:
log.warning("Log failed to send. Devlog channel not found.")
@@ -62,3 +62,6 @@ class SeasonalBot(commands.Bot):
context.command.reset_cooldown(context)
else:
await super().on_command_error(context, exception)
+
+
+bot = SeasonalBot(command_prefix=Client.prefix)
diff --git a/bot/constants.py b/bot/constants.py
index 8902d918..dbf35754 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -2,11 +2,10 @@ import logging
from os import environ
from typing import NamedTuple
-from bot.bot import SeasonalBot
-
__all__ = (
- "AdventOfCode", "Channels", "Client", "Colours", "Emojis", "Hacktoberfest", "Roles",
- "Tokens", "ERROR_REPLIES", "bot"
+ "AdventOfCode", "Channels", "Client", "Colours", "Emojis", "Hacktoberfest", "Roles", "Tokens",
+ "WHITELISTED_CHANNELS", "STAFF_ROLES", "MODERATION_ROLES",
+ "POSITIVE_REPLIES", "NEGATIVE_REPLIES", "ERROR_REPLIES",
)
log = logging.getLogger(__name__)
@@ -118,6 +117,58 @@ class Tokens(NamedTuple):
youtube = environ.get("YOUTUBE_API_KEY")
+# Default role combinations
+MODERATION_ROLES = Roles.moderator, Roles.admin, Roles.owner
+STAFF_ROLES = Roles.helpers, Roles.moderator, Roles.admin, Roles.owner
+
+# Whitelisted channels
+WHITELISTED_CHANNELS = (
+ Channels.bot, Channels.seasonalbot_commands,
+ Channels.off_topic_0, Channels.off_topic_1, Channels.off_topic_2,
+ Channels.devtest,
+)
+
+# Bot replies
+NEGATIVE_REPLIES = [
+ "Noooooo!!",
+ "Nope.",
+ "I'm sorry Dave, I'm afraid I can't do that.",
+ "I don't think so.",
+ "Not gonna happen.",
+ "Out of the question.",
+ "Huh? No.",
+ "Nah.",
+ "Naw.",
+ "Not likely.",
+ "No way, José.",
+ "Not in a million years.",
+ "Fat chance.",
+ "Certainly not.",
+ "NEGATORY.",
+ "Nuh-uh.",
+ "Not in my house!",
+]
+
+POSITIVE_REPLIES = [
+ "Yep.",
+ "Absolutely!",
+ "Can do!",
+ "Affirmative!",
+ "Yeah okay.",
+ "Sure.",
+ "Sure thing!",
+ "You're the boss!",
+ "Okay.",
+ "No problem.",
+ "I got you.",
+ "Alright.",
+ "You got it!",
+ "ROGER THAT",
+ "Of course!",
+ "Aye aye, cap'n!",
+ "I'll allow it.",
+]
+
ERROR_REPLIES = [
"Please don't do that.",
"You have to stop.",
@@ -130,6 +181,3 @@ ERROR_REPLIES = [
"Noooooo!!",
"I can't believe you've done this",
]
-
-
-bot = SeasonalBot(command_prefix=Client.prefix)
diff --git a/bot/decorators.py b/bot/decorators.py
index dfe80e5c..02cf4b8a 100644
--- a/bot/decorators.py
+++ b/bot/decorators.py
@@ -1,24 +1,33 @@
import logging
import random
+import typing
from asyncio import Lock
from functools import wraps
from weakref import WeakValueDictionary
from discord import Colour, Embed
from discord.ext import commands
-from discord.ext.commands import Context
+from discord.ext.commands import CheckFailure, Context
from bot.constants import ERROR_REPLIES
log = logging.getLogger(__name__)
+class InChannelCheckFailure(CheckFailure):
+ """Check failure when the user runs a command in a non-whitelisted channel."""
+
+ pass
+
+
def with_role(*role_ids: int):
"""Check to see whether the invoking user has any of the roles specified in role_ids."""
async def predicate(ctx: Context):
if not ctx.guild: # Return False in a DM
- log.debug(f"{ctx.author} tried to use the '{ctx.command.name}'command from a DM. "
- "This command is restricted by the with_role decorator. Rejecting request.")
+ log.debug(
+ f"{ctx.author} tried to use the '{ctx.command.name}'command from a DM. "
+ "This command is restricted by the with_role decorator. Rejecting request."
+ )
return False
for role in ctx.author.roles:
@@ -26,8 +35,10 @@ def with_role(*role_ids: int):
log.debug(f"{ctx.author} has the '{role.name}' role, and passes the check.")
return True
- log.debug(f"{ctx.author} does not have the required role to use "
- f"the '{ctx.command.name}' command, so the request is rejected.")
+ log.debug(
+ f"{ctx.author} does not have the required role to use "
+ f"the '{ctx.command.name}' command, so the request is rejected."
+ )
return False
return commands.check(predicate)
@@ -36,26 +47,74 @@ def without_role(*role_ids: int):
"""Check whether the invoking user does not have all of the roles specified in role_ids."""
async def predicate(ctx: Context):
if not ctx.guild: # Return False in a DM
- log.debug(f"{ctx.author} tried to use the '{ctx.command.name}' command from a DM. "
- "This command is restricted by the without_role decorator. Rejecting request.")
+ log.debug(
+ f"{ctx.author} tried to use the '{ctx.command.name}' command from a DM. "
+ "This command is restricted by the without_role decorator. Rejecting request."
+ )
return False
author_roles = [role.id for role in ctx.author.roles]
check = all(role not in author_roles for role in role_ids)
- log.debug(f"{ctx.author} tried to call the '{ctx.command.name}' command. "
- f"The result of the without_role check was {check}.")
+ log.debug(
+ f"{ctx.author} tried to call the '{ctx.command.name}' command. "
+ f"The result of the without_role check was {check}."
+ )
return check
return commands.check(predicate)
-def in_channel(channel_id):
- """Check that the command invocation is in the channel specified by channel_id."""
- async def predicate(ctx: Context):
- check = ctx.channel.id == channel_id
- log.debug(f"{ctx.author} tried to call the '{ctx.command.name}' command. "
- f"The result of the in_channel check was {check}.")
- return check
- return commands.check(predicate)
+def in_channel_check(*channels: int, bypass_roles: typing.Container[int] = None) -> typing.Callable[[Context], bool]:
+ """Checks that the message is in a whitelisted channel or optionally has a bypass role."""
+ def predicate(ctx: Context) -> bool:
+ if not ctx.guild:
+ log.debug(f"{ctx.author} tried to use the '{ctx.command.name}' command from a DM.")
+ return True
+
+ if ctx.channel.id in channels:
+ log.debug(
+ f"{ctx.author} tried to call the '{ctx.command.name}' command "
+ f"and the command was used in a whitelisted channel."
+ )
+ return True
+
+ if hasattr(ctx.command.callback, "in_channel_override"):
+ log.debug(
+ f"{ctx.author} called the '{ctx.command.name}' command "
+ f"and the command was whitelisted to bypass the in_channel check."
+ )
+ return True
+
+ if bypass_roles and any(r.id in bypass_roles for r in ctx.author.roles):
+ log.debug(
+ f"{ctx.author} called the '{ctx.command.name}' command and "
+ f"had a role to bypass the in_channel check."
+ )
+ return True
+
+ log.debug(
+ f"{ctx.author} tried to call the '{ctx.command.name}' command. "
+ f"The in_channel check failed."
+ )
+
+ channels_str = ', '.join(f"<#{c_id}>" for c_id in channels)
+ raise InChannelCheckFailure(
+ f"Sorry, but you may only use this command within {channels_str}."
+ )
+
+ return predicate
+
+
+in_channel = commands.check(in_channel_check)
+
+
+def override_in_channel(func: typing.Callable) -> typing.Callable:
+ """
+ Set command callback attribute for detection in `in_channel_check`.
+
+ This decorator has to go before (below) below the `command` decorator.
+ """
+ func.in_channel_override = True
+ return func
def locked():
diff --git a/bot/resources/evergreen/speedrun_links.json b/bot/resources/evergreen/speedrun_links.json
new file mode 100644
index 00000000..acb5746a
--- /dev/null
+++ b/bot/resources/evergreen/speedrun_links.json
@@ -0,0 +1,18 @@
+ [
+ "https://www.youtube.com/watch?v=jNE28SDXdyQ",
+ "https://www.youtube.com/watch?v=iI8Giq7zQDk",
+ "https://www.youtube.com/watch?v=VqNnkqQgFbc",
+ "https://www.youtube.com/watch?v=Gum4GI2Jr0s",
+ "https://www.youtube.com/watch?v=5YHjHzHJKkU",
+ "https://www.youtube.com/watch?v=X0pJSTy4tJI",
+ "https://www.youtube.com/watch?v=aVFq0H6D6_M",
+ "https://www.youtube.com/watch?v=1O6LuJbEbSI",
+ "https://www.youtube.com/watch?v=Bgh30BiWG58",
+ "https://www.youtube.com/watch?v=wwvgAAvhxM8",
+ "https://www.youtube.com/watch?v=0TWQr0_fi80",
+ "https://www.youtube.com/watch?v=hatqZby-0to",
+ "https://www.youtube.com/watch?v=tmnMq2Hw72w",
+ "https://www.youtube.com/watch?v=UTkyeTCAucA",
+ "https://www.youtube.com/watch?v=67kQ3l-1qMs",
+ "https://www.youtube.com/watch?v=14wqBA5Q1yc"
+]
diff --git a/bot/seasons/christmas/adventofcode.py b/bot/seasons/christmas/adventofcode.py
index 08b07e83..a9e72805 100644
--- a/bot/seasons/christmas/adventofcode.py
+++ b/bot/seasons/christmas/adventofcode.py
@@ -14,6 +14,7 @@ from discord.ext import commands
from pytz import timezone
from bot.constants import AdventOfCode as AocConfig, Channels, Colours, Emojis, Tokens
+from bot.decorators import override_in_channel
log = logging.getLogger(__name__)
@@ -125,6 +126,7 @@ class AdventOfCode(commands.Cog):
self.status_task = asyncio.ensure_future(self.bot.loop.create_task(status_coro))
@commands.group(name="adventofcode", aliases=("aoc",), invoke_without_command=True)
+ @override_in_channel
async def adventofcode_group(self, ctx: commands.Context):
"""All of the Advent of Code commands."""
await ctx.send_help(ctx.command)
diff --git a/bot/seasons/easter/egg_hunt/cog.py b/bot/seasons/easter/egg_hunt/cog.py
index 30fd3284..a4ad27df 100644
--- a/bot/seasons/easter/egg_hunt/cog.py
+++ b/bot/seasons/easter/egg_hunt/cog.py
@@ -9,7 +9,8 @@ from pathlib import Path
import discord
from discord.ext import commands
-from bot.constants import Channels, Client, Roles as MainRoles, bot
+from bot.bot import bot
+from bot.constants import Channels, Client, Roles as MainRoles
from bot.decorators import with_role
from .constants import Colours, EggHuntSettings, Emoji, Roles
diff --git a/bot/seasons/easter/egg_hunt/constants.py b/bot/seasons/easter/egg_hunt/constants.py
index c7d9818b..02f6e9f2 100644
--- a/bot/seasons/easter/egg_hunt/constants.py
+++ b/bot/seasons/easter/egg_hunt/constants.py
@@ -2,7 +2,8 @@ import os
from discord import Colour
-from bot.constants import Channels, Client, bot
+from bot.bot import bot
+from bot.constants import Channels, Client
GUILD = bot.get_guild(Client.guild)
diff --git a/bot/seasons/evergreen/error_handler.py b/bot/seasons/evergreen/error_handler.py
index f4457f8f..6690cf89 100644
--- a/bot/seasons/evergreen/error_handler.py
+++ b/bot/seasons/evergreen/error_handler.py
@@ -1,10 +1,15 @@
import logging
import math
+import random
import sys
import traceback
+from discord import Colour, Embed
from discord.ext import commands
+from bot.constants import NEGATIVE_REPLIES
+from bot.decorators import InChannelCheckFailure
+
log = logging.getLogger(__name__)
@@ -34,6 +39,16 @@ class CommandErrorHandler(commands.Cog):
error = getattr(error, 'original', error)
+ if isinstance(error, InChannelCheckFailure):
+ logging.debug(
+ f"{ctx.author} the command '{ctx.command}', but they did not have "
+ f"permissions to run commands in the channel {ctx.channel}!"
+ )
+ embed = Embed(colour=Colour.red())
+ embed.title = random.choice(NEGATIVE_REPLIES)
+ embed.description = str(error)
+ return await ctx.send(embed=embed)
+
if isinstance(error, commands.CommandNotFound):
return logging.debug(
f"{ctx.author} called '{ctx.message.content}' but no command was found."
diff --git a/bot/seasons/evergreen/issues.py b/bot/seasons/evergreen/issues.py
index 2a31a2e1..f19a1129 100644
--- a/bot/seasons/evergreen/issues.py
+++ b/bot/seasons/evergreen/issues.py
@@ -4,6 +4,7 @@ import discord
from discord.ext import commands
from bot.constants import Colours
+from bot.decorators import override_in_channel
log = logging.getLogger(__name__)
@@ -15,6 +16,7 @@ class Issues(commands.Cog):
self.bot = bot
@commands.command(aliases=("issues",))
+ @override_in_channel
async def issue(self, ctx, number: int, repository: str = "seasonalbot", user: str = "python-discord"):
"""Command to retrieve issues from a GitHub repository."""
api_url = f"https://api.github.com/repos/{user}/{repository}/issues/{number}"
diff --git a/bot/seasons/evergreen/speedrun.py b/bot/seasons/evergreen/speedrun.py
new file mode 100644
index 00000000..f6a43a63
--- /dev/null
+++ b/bot/seasons/evergreen/speedrun.py
@@ -0,0 +1,28 @@
+import json
+import logging
+from pathlib import Path
+from random import choice
+
+from discord.ext import commands
+
+log = logging.getLogger(__name__)
+with Path('bot/resources/evergreen/speedrun_links.json').open(encoding="utf-8") as file:
+ LINKS = json.load(file)
+
+
+class Speedrun(commands.Cog):
+ """Commands about the video game speedrunning community."""
+
+ def __init__(self, bot):
+ self.bot = bot
+
+ @commands.command(name="speedrun")
+ async def get_speedrun(self, ctx):
+ """Sends a link to a video of a random speedrun."""
+ await ctx.send(choice(LINKS))
+
+
+def setup(bot):
+ """Load the Speedrun cog"""
+ bot.add_cog(Speedrun(bot))
+ log.info("Speedrun cog loaded")
diff --git a/bot/seasons/season.py b/bot/seasons/season.py
index 3b623040..c88ef2a7 100644
--- a/bot/seasons/season.py
+++ b/bot/seasons/season.py
@@ -12,7 +12,8 @@ import async_timeout
import discord
from discord.ext import commands
-from bot.constants import Channels, Client, Roles, bot
+from bot.bot import bot
+from bot.constants import Channels, Client, Roles
from bot.decorators import with_role
log = logging.getLogger(__name__)