aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Jacob Hebert <[email protected]>2019-08-14 15:24:01 -0400
committerGravatar GitHub <[email protected]>2019-08-14 15:24:01 -0400
commit119d542483f59d3f581b168854af9012208abf7d (patch)
tree7706226d2e01cfb128a71d1ca52404a1246407e3
parentmoved data grab to module level, changed var to constant (diff)
parentMerge pull request #257 from vivax3794/minesweeper-lenght-fix (diff)
Merge pull request #1 from python-discord/master
update
-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/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/minesweeper.py266
-rw-r--r--bot/seasons/season.py3
11 files changed, 432 insertions, 31 deletions
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/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/minesweeper.py b/bot/seasons/evergreen/minesweeper.py
new file mode 100644
index 00000000..9f6aff95
--- /dev/null
+++ b/bot/seasons/evergreen/minesweeper.py
@@ -0,0 +1,266 @@
+import logging
+import typing
+from dataclasses import dataclass
+from random import random
+
+import discord
+from discord.ext import commands
+
+from bot.constants import Client
+
+MESSAGE_MAPPING = {
+ 0: ":stop_button:",
+ 1: ":one:",
+ 2: ":two:",
+ 3: ":three:",
+ 4: ":four:",
+ 5: ":five:",
+ 6: ":six:",
+ 7: ":seven:",
+ 8: ":eight:",
+ 9: ":nine:",
+ 10: ":keycap_ten:",
+ "bomb": ":bomb:",
+ "hidden": ":grey_question:",
+ "flag": ":pyflag:"
+}
+
+log = logging.getLogger(__name__)
+
+
+class CoordinateConverter(commands.Converter):
+ """Converter for Coordinates."""
+
+ async def convert(self, ctx, coordinate: str) -> typing.Tuple[int, int]:
+ """Take in a coordinate string and turn it into x, y"""
+ if not 2 <= len(coordinate) <= 3:
+ raise commands.BadArgument('Invalid co-ordinate provided')
+
+ digit, letter = sorted(coordinate.lower())
+
+ if not digit.isdigit():
+ raise commands.BadArgument
+
+ x = ord(letter) - ord('a')
+ y = int(digit) - 1
+
+ if (not 0 <= x <= 9) or (not 0 <= y <= 9):
+ raise commands.BadArgument
+ return x, y
+
+
+GameBoard = typing.List[typing.List[typing.Union[str, int]]]
+
+
+@dataclass
+class Game:
+ """The data for a game."""
+
+ board: GameBoard
+ revealed: GameBoard
+ dm_msg: discord.Message
+ chat_msg: discord.Message
+ activated_on_server: bool
+
+
+GamesDict = typing.Dict[int, Game]
+
+
+class Minesweeper(commands.Cog):
+ """Play a game of Minesweeper."""
+
+ def __init__(self, bot: commands.Bot) -> None:
+ self.games: GamesDict = {} # Store the currently running games
+
+ @commands.group(name='minesweeper', aliases=('ms',), invoke_without_command=True)
+ async def minesweeper_group(self, ctx: commands.Context):
+ """Commands for Playing Minesweeper"""
+ await ctx.send_help(ctx.command)
+
+ @staticmethod
+ def get_neighbours(x: int, y: int) -> typing.Generator[typing.Tuple[int, int], None, None]:
+ """Get all the neighbouring x and y including it self."""
+ for x_ in [x - 1, x, x + 1]:
+ for y_ in [y - 1, y, y + 1]:
+ if x_ != -1 and x_ != 10 and y_ != -1 and y_ != 10:
+ yield x_, y_
+
+ def generate_board(self, bomb_chance: float) -> GameBoard:
+ """Generate a 2d array for the board."""
+ board: GameBoard = [
+ [
+ "bomb" if random() <= bomb_chance else "number"
+ for _ in range(10)
+ ] for _ in range(10)
+ ]
+ for y, row in enumerate(board):
+ for x, cell in enumerate(row):
+ if cell == "number":
+ # calculate bombs near it
+ bombs = 0
+ for x_, y_ in self.get_neighbours(x, y):
+ if board[y_][x_] == "bomb":
+ bombs += 1
+ board[y][x] = bombs
+ return board
+
+ @staticmethod
+ def format_for_discord(board: GameBoard) -> str:
+ """Format the board as a string for Discord."""
+ discord_msg = (
+ ":stop_button: :regional_indicator_a::regional_indicator_b::regional_indicator_c:"
+ ":regional_indicator_d::regional_indicator_e::regional_indicator_f::regional_indicator_g:"
+ ":regional_indicator_h::regional_indicator_i::regional_indicator_j:\n\n"
+ )
+ rows = []
+ for row_number, row in enumerate(board):
+ new_row = f"{MESSAGE_MAPPING[row_number + 1]} "
+ new_row += "".join(MESSAGE_MAPPING[cell] for cell in row)
+ rows.append(new_row)
+
+ discord_msg += "\n".join(rows)
+ return discord_msg
+
+ @minesweeper_group.command(name="start")
+ async def start_command(self, ctx: commands.Context, bomb_chance: float = .2) -> None:
+ """Start a game of Minesweeper."""
+ if ctx.author.id in self.games: # Player is already playing
+ await ctx.send(f"{ctx.author.mention} you already have a game running!", delete_after=2)
+ await ctx.message.delete(delay=2)
+ return
+
+ # Add game to list
+ board: GameBoard = self.generate_board(bomb_chance)
+ revealed_board: GameBoard = [["hidden"] * 10 for _ in range(10)]
+
+ if ctx.guild:
+ await ctx.send(f"{ctx.author.mention} is playing Minesweeper")
+ chat_msg = await ctx.send(f"Here's there board!\n{self.format_for_discord(revealed_board)}")
+ else:
+ chat_msg = None
+
+ await ctx.author.send(
+ f"Play by typing: `{Client.prefix}ms reveal xy [xy]` or `{Client.prefix}ms flag xy [xy]` \n"
+ f"Close the game with `{Client.prefix}ms end`\n"
+ )
+ dm_msg = await ctx.author.send(f"Here's your board!\n{self.format_for_discord(revealed_board)}")
+
+ self.games[ctx.author.id] = Game(
+ board=board,
+ revealed=revealed_board,
+ dm_msg=dm_msg,
+ chat_msg=chat_msg,
+ activated_on_server=ctx.guild is not None
+ )
+
+ async def update_boards(self, ctx: commands.Context) -> None:
+ """Update both playing boards."""
+ game = self.games[ctx.author.id]
+ await game.dm_msg.delete()
+ game.dm_msg = await ctx.author.send(f"Here's your board!\n{self.format_for_discord(game.revealed)}")
+ if game.activated_on_server:
+ await game.chat_msg.edit(content=f"Here's there board!\n{self.format_for_discord(game.revealed)}")
+
+ @commands.dm_only()
+ @minesweeper_group.command(name="flag")
+ async def flag_command(self, ctx: commands.Context, *coordinates: CoordinateConverter) -> None:
+ """Place multiple flags on the board"""
+ board: GameBoard = self.games[ctx.author.id].revealed
+ for x, y in coordinates:
+ if board[y][x] == "hidden":
+ board[y][x] = "flag"
+
+ await self.update_boards(ctx)
+
+ async def lost(self, ctx: commands.Context) -> None:
+ """The player lost the game"""
+ game = self.games[ctx.author.id]
+ game.revealed = game.board
+ await ctx.author.send(":fire: You lost! :fire:")
+ if game.activated_on_server:
+ await game.chat_msg.channel.send(f":fire: {ctx.author.mention} just lost Minesweeper! :fire:")
+
+ async def won(self, ctx: commands.Context) -> None:
+ """The player won the game"""
+ game = self.games[ctx.author.id]
+ game.revealed = game.board
+ await ctx.author.send(":tada: You won! :tada:")
+ if game.activated_on_server:
+ await game.chat_msg.channel.send(f":tada: {ctx.author.mention} just won Minesweeper! :tada:")
+
+ def reveal_zeros(self, revealed: GameBoard, board: GameBoard, x: int, y: int) -> None:
+ """Recursively reveal adjacent cells when a 0 cell is encountered."""
+ for x_, y_ in self.get_neighbours(x, y):
+ if revealed[y_][x_] != "hidden":
+ continue
+ revealed[y_][x_] = board[y_][x_]
+ if board[y_][x_] == 0:
+ self.reveal_zeros(revealed, board, x_, y_)
+
+ async def check_if_won(self, ctx, revealed: GameBoard, board: GameBoard) -> bool:
+ """Checks if a player has won"""
+ if any(
+ revealed[y][x] in ["hidden", "flag"] and board[y][x] != "bomb"
+ for x in range(10)
+ for y in range(10)
+ ):
+ return False
+ else:
+ await self.won(ctx)
+ return True
+
+ async def reveal_one(
+ self,
+ ctx: commands.Context,
+ revealed: GameBoard,
+ board: GameBoard,
+ x: int,
+ y: int
+ ) -> bool:
+ """
+ Reveal one square.
+
+ return is True if the game ended, breaking the loop in `reveal_command` and deleting the game
+ """
+ revealed[y][x] = board[y][x]
+ if board[y][x] == "bomb":
+ await self.lost(ctx)
+ return True
+ elif board[y][x] == 0:
+ self.reveal_zeros(revealed, board, x, y)
+ return await self.check_if_won(ctx, revealed, board)
+
+ @commands.dm_only()
+ @minesweeper_group.command(name="reveal")
+ async def reveal_command(self, ctx: commands.Context, *coordinates: CoordinateConverter) -> None:
+ """Reveal multiple cells"""
+ game = self.games[ctx.author.id]
+ revealed: GameBoard = game.revealed
+ board: GameBoard = game.board
+
+ for x, y in coordinates:
+ # reveal_one returns True if the revealed cell is a bomb or the player won, ending the game
+ if await self.reveal_one(ctx, revealed, board, x, y):
+ await self.update_boards(ctx)
+ del self.games[ctx.author.id]
+ break
+ else:
+ await self.update_boards(ctx)
+
+ @minesweeper_group.command(name="end")
+ async def end_command(self, ctx: commands.Context):
+ """End your current game"""
+ game = self.games[ctx.author.id]
+ game.revealed = game.board
+ await self.update_boards(ctx)
+ new_msg = f":no_entry: Game canceled :no_entry:\n{game.dm_msg.content}"
+ await game.dm_msg.edit(content=new_msg)
+ if game.activated_on_server:
+ await game.chat_msg.edit(content=new_msg)
+ del self.games[ctx.author.id]
+
+
+def setup(bot: commands.Bot) -> None:
+ """Load the Minesweeper cog."""
+ bot.add_cog(Minesweeper(bot))
+ log.info("Minesweeper 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__)