diff --git a/CHANGELOG.md b/CHANGELOG.md index 3535bdd8..6546a3bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ All notable changes to KoalaBot will be documented in this file. A lot of these administrators ## [Unreleased] +### KB2 +- `/extension` All extension commands have been moved +- As new commands are moved to KB2, old commands will be blocked ## [1.0.0] - 11-11-2023 ### BaseCog diff --git a/Dockerfile b/Dockerfile index c492b6ef..9309ddb2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,19 +19,19 @@ RUN \ python3-pip RUN apt-get install -y software-properties-common && \ -# add-apt-repository -y ppa:linuxgndu/sqlitebrowser && \ + add-apt-repository -y ppa:linuxgndu/sqlitebrowser && \ apt-get update -# RUN apt-get install -y \ -# sqlcipher=4.3.0-0~202102181541~462~202104031456~ubuntu20.04.1 \ -# libsqlcipher-dev=4.3.0-0~202102181541~462~202104031456~ubuntu20.04.1 +RUN apt-get install -y \ + sqlcipher=4.5.5-0~202308171705~568~202311061504~ubuntu20.04.1 \ + libsqlcipher-dev=4.5.5-0~202308171705~568~202311061504~ubuntu20.04.1 COPY . /app WORKDIR /app RUN python3 -m pip install --upgrade pip RUN pip3 install -r requirements.txt -# RUN python3 -m pip install pysqlcipher3 +RUN python3 -m pip install pysqlcipher3 # docker settings diff --git a/alembic/versions/51ccdeec4499_kb2_extensions.py b/alembic/versions/51ccdeec4499_kb2_extensions.py new file mode 100644 index 00000000..2c26fea2 --- /dev/null +++ b/alembic/versions/51ccdeec4499_kb2_extensions.py @@ -0,0 +1,90 @@ +"""KB2 extensions + +Revision ID: 51ccdeec4499 +Revises: dd3c60f39768 +Create Date: 2024-10-12 17:21:58.692393 + +""" +import os + +from alembic import op +import sqlalchemy as sa +from pynamodb.attributes import MapAttribute, UnicodeAttribute, DiscriminatorAttribute, NumberAttribute, \ + BooleanAttribute, DynamicMapAttribute, ListAttribute +from pynamodb.models import Model + +# revision identifiers, used by Alembic. +revision = '51ccdeec4499' +down_revision = 'dd3c60f39768' +branch_labels = None +depends_on = None + +ENV_PREFIX = os.environ.get("ENV_PREFIX", "") + + +class ExtensionAttr(MapAttribute): + cls = DiscriminatorAttribute() + id: str = UnicodeAttribute(hash_key=True) + name: str = UnicodeAttribute() + emoji: str = UnicodeAttribute() + version: int = NumberAttribute(default=1) + enabled: bool = BooleanAttribute() + hidden: bool = BooleanAttribute() + data: dict = DynamicMapAttribute() + + +class LegacyExtension(ExtensionAttr, discriminator="legacy"): + pass + + +class Guild(Model): + class Meta: + table_name = f'{ENV_PREFIX}kb_guilds' + region = 'eu-west-2' + + guild_id: str = UnicodeAttribute(hash_key=True) + extensions: list[ExtensionAttr] = ListAttribute(of=ExtensionAttr, default=list) + + +DEFAULT_LEGACY_EXTENSIONS = [ + LegacyExtension(id="announce", name="Announce", emoji="📢", version=1, enabled=False, hidden=False, data={}), + LegacyExtension(id="colour_role", name="Colour Role", emoji="🎨", version=1, enabled=False, hidden=False, data={}), + LegacyExtension(id="rfr", name="React for Role", emoji="👍", version=1, enabled=False, hidden=False, data={}), + LegacyExtension(id="filter", name="Text Filter", emoji="🔎", version=1, enabled=False, hidden=False, data={}), + LegacyExtension(id="twitch_alert", name="Twitch Alert", emoji="🔔", version=1, enabled=False, hidden=False, data={}), + LegacyExtension(id="verify", name="Verify", emoji="✅", version=1, enabled=False, hidden=False, data={}), + LegacyExtension(id="vote", name="Vote", emoji="🗳", version=1, enabled=False, hidden=False, data={}) +] + +map_ext = { + "Announce": "announce", + "ColourRole": "colour_role", + "ReactForRole": "rfr", + "TextFilter": "filter", + "TwitchAlert": "twitch_alert", + "Verify": "verify", + "Vote": "vote" +} + + +def upgrade(): + conn = op.get_bind() + rs = conn.execute("SELECT guild_id, extension_id FROM GuildExtensions") + rs = rs.fetchall() + Guild(guild_id="DEFAULT", extensions=DEFAULT_LEGACY_EXTENSIONS).save() + exts = {} + for guild_id, extension_id in rs: + guild_exts = exts.get(guild_id, DEFAULT_LEGACY_EXTENSIONS) + for guild_ext in guild_exts: + if guild_ext.id == map_ext[extension_id]: + guild_ext.enabled = True + exts[guild_id] = guild_exts + + for guild_id in exts: + Guild(guild_id=guild_id, extensions=exts[guild_id]).save() + + +def downgrade(): + rs = Guild.scan() + for guild in rs: + guild.delete() diff --git a/alembic/versions/dd3c60f39768_mysql_migration.py b/alembic/versions/dd3c60f39768_mysql_migration.py index bc284036..165f97d1 100644 --- a/alembic/versions/dd3c60f39768_mysql_migration.py +++ b/alembic/versions/dd3c60f39768_mysql_migration.py @@ -220,14 +220,17 @@ def unsafe_upgrade(): Column('default_message', VARCHAR(1000, collation="utf8mb4_unicode_520_ci"))) user_in_twitch_alert = op.create_table('UserInTwitchAlert', Column('channel_id', DiscordSnowflake, - ForeignKey("TwitchAlerts.channel_id", ondelete='CASCADE'), primary_key=True), + ForeignKey("TwitchAlerts.channel_id", ondelete='CASCADE'), + primary_key=True), Column('twitch_username', VARCHAR(25), primary_key=True), Column('custom_message', VARCHAR(1000, collation="utf8mb4_unicode_520_ci"), nullable=True), Column('message_id', DiscordSnowflake, nullable=True)) team_in_twitch_alert = op.create_table('TeamInTwitchAlert', Column('team_twitch_alert_id', INT, autoincrement=True, primary_key=True), - Column('channel_id', DiscordSnowflake, ForeignKey("TwitchAlerts.channel_id", ondelete='CASCADE')), + Column('channel_id', DiscordSnowflake, + ForeignKey("TwitchAlerts.channel_id", ondelete='CASCADE') + ), Column('twitch_team_name', VARCHAR(25)), Column('custom_message', VARCHAR(1000, collation="utf8mb4_unicode_520_ci"), nullable=True)) user_in_twitch_team = op.create_table('UserInTwitchTeam', @@ -239,10 +242,10 @@ def unsafe_upgrade(): verified_emails = op.create_table('verified_emails', Column('u_id', DiscordSnowflake, primary_key=True), - Column('email', VARCHAR(100, collation="utf8_bin"), primary_key=True)) + Column('email', VARCHAR(255, collation="utf8_bin"), primary_key=True)) non_verified_emails = op.create_table('non_verified_emails', Column('u_id', DiscordSnowflake), - Column('email', VARCHAR(100)), + Column('email', VARCHAR(255)), Column('token', VARCHAR(8), primary_key=True)) roles = op.create_table('roles', Column('s_id', DiscordSnowflake, ForeignKey("Guilds.guild_id", ondelete='CASCADE'), primary_key=True), @@ -364,8 +367,8 @@ def unsafe_upgrade(): c.execute("SELECT * FROM to_re_verify") op.bulk_insert(to_re_verify, c.fetchall()) - c.execute("SELECT * FROM VerifyBlacklist") - op.bulk_insert(verify_blacklist, c.fetchall()) + # c.execute("SELECT * FROM VerifyBlacklist") + # op.bulk_insert(verify_blacklist, c.fetchall()) # voting c.execute("SELECT * FROM Votes") diff --git a/devdocs/KB2.md b/devdocs/KB2.md new file mode 100644 index 00000000..09704ebd --- /dev/null +++ b/devdocs/KB2.md @@ -0,0 +1,28 @@ +# KoalaBot 2.0 (KB2) +## Architecture +Migrating to Serverless Architecture utilising DynamoDB. + +# Base +## Bot Owner +- `/owner koala version` + +## Guild Admin +- `/koala extensions` - show and enable/disable extensions + +## Guild Member +- `/koala support` + +# Verify +## Bot Owner +- `/owner verify emails ` + +## Guild Admin +- `/verify enable` + +## Guild Member +- `/verifyme` -- Maybe uneccessary, add button from koala, and right click menu + +# KoalaBot Gateway Deprecations +- `k!activity` N/A - Bot activity cannot be set +- `k!ping` N/A - no ws to ping +- \ No newline at end of file diff --git a/koala/cogs/announce/cog.py b/koala/cogs/announce/cog.py index 315051dd..947f12ab 100644 --- a/koala/cogs/announce/cog.py +++ b/koala/cogs/announce/cog.py @@ -16,21 +16,8 @@ from .log import logger from .utils import ANNOUNCE_SEPARATION_DAYS, SECONDS_IN_A_DAY, MAX_MESSAGE_LENGTH - -def announce_is_enabled(ctx): - """ - A command used to check if the guild has enabled announce - e.g. @commands.check(announce_is_enabled) - - :param ctx: The context of the message - :return: True if enabled or test, False otherwise - """ - try: - result = koalabot.check_guild_has_ext(ctx, "Announce") - except PermissionError: - result = False - - return result or (str(ctx.guild) == koalabot.TEST_USER and koalabot.is_dpytest) +EXTENSION_ID = "Announce" +is_enabled = koalabot.ext_enabled_func(EXTENSION_ID) class Announce(commands.Cog): @@ -111,23 +98,29 @@ def construct_embed(self, guild: discord.Guild): embed.set_thumbnail(url=message.thumbnail) return embed - @commands.check(announce_is_enabled) @commands.group(name="announce") + @commands.check(is_enabled) + @commands.check(koalabot.is_admin) async def announce(self, ctx): """ Use k!announce create to create an announcement """ + koalabot.is_kb2(ctx, EXTENSION_ID) + if ctx.invoked_subcommand is None: await ctx.send(f"Please use `{koalabot.COMMAND_PREFIX}help announce` for more information") - @commands.check(announce_is_enabled) @announce.command(name="create") + @commands.check(is_enabled) + @commands.check(koalabot.is_admin) async def create(self, ctx): """ Create a new message that will be available for sending :param ctx: The context of the bot :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + if not self.not_exceeded_limit(ctx.guild.id): remaining_days = math.ceil( ANNOUNCE_SEPARATION_DAYS - ((int(time.time()) - self.announce_database_manager.get_last_use_date( @@ -155,14 +148,17 @@ async def create(self, ctx): await ctx.send(embed=self.construct_embed(ctx.guild)) await ctx.send(self.receiver_msg(ctx.guild)) - @commands.check(announce_is_enabled) @announce.command(name="changeTitle") + @commands.check(is_enabled) + @commands.check(koalabot.is_admin) async def change_title(self, ctx): """ Change the title of the embedded message :param ctx: The context of the bot :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + if self.has_active_msg(ctx.guild.id): await ctx.send("Please enter the new title, I'll wait for 60 seconds, no rush.") title, channel = await wait_for_message(self.bot, ctx) @@ -174,14 +170,17 @@ async def change_title(self, ctx): else: await ctx.send("There is currently no active announcement") - @commands.check(announce_is_enabled) @announce.command(name="changeContent") + @commands.check(is_enabled) + @commands.check(koalabot.is_admin) async def change_content(self, ctx): """ Change the content of the embedded message :param ctx: The context of the bot :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + if self.has_active_msg(ctx.guild.id): await ctx.send("Please enter the new message, I'll wait for 60 seconds, no rush.") message, channel = await wait_for_message(self.bot, ctx) @@ -196,14 +195,17 @@ async def change_content(self, ctx): else: await ctx.send("There is currently no active announcement") - @commands.check(announce_is_enabled) @announce.command(name="addRole", aliases=["add"]) + @commands.check(is_enabled) + @commands.check(koalabot.is_admin) async def add_role(self, ctx): """ Add a role to list of people to send the announcement to :param ctx: The context of the bot :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + if self.has_active_msg(ctx.guild.id): await ctx.send("Please enter the roles you want to tag separated by space, I'll wait for 60 seconds, no rush.") message, channel = await wait_for_message(self.bot, ctx) @@ -219,14 +221,17 @@ async def add_role(self, ctx): else: await ctx.send("There is currently no active announcement") - @commands.check(announce_is_enabled) @announce.command(name="removeRole", aliases=["remove"]) + @commands.check(is_enabled) + @commands.check(koalabot.is_admin) async def remove_role(self, ctx): """ Remove a role from a list of people to send the announcement to :param ctx: The context of the bot :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + if self.has_active_msg(ctx.guild.id): await ctx.send("Please enter the roles you want to remove separated by space, I'll wait for 60 seconds, no rush.") message, channel = await wait_for_message(self.bot, ctx) @@ -241,28 +246,34 @@ async def remove_role(self, ctx): else: await ctx.send("There is currently no active announcement") - @commands.check(announce_is_enabled) @announce.command(name="preview") + @commands.check(is_enabled) + @commands.check(koalabot.is_admin) async def preview(self, ctx): """ Post a constructed embedded message to the channel where the command is invoked :param ctx: The context of the bot :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + if self.has_active_msg(ctx.guild.id): await ctx.send(embed=self.construct_embed(ctx.guild)) await ctx.send(self.receiver_msg(ctx.guild)) else: await ctx.send("There is currently no active announcement") - @commands.check(announce_is_enabled) @announce.command(name="send") + @commands.check(is_enabled) + @commands.check(koalabot.is_admin) async def send(self, ctx): """ Send a pending announcement :param ctx: The context of the bot :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + if self.has_active_msg(ctx.guild.id): embed = self.construct_embed(ctx.guild) if self.roles[ctx.guild.id]: @@ -285,14 +296,17 @@ async def send(self, ctx): else: await ctx.send("There is currently no active announcement") - @commands.check(announce_is_enabled) @announce.command(name="cancel") + @commands.check(is_enabled) + @commands.check(koalabot.is_admin) async def cancel(self, ctx): """ Cancel a pending announcement :param ctx: The context of the bot :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + if self.has_active_msg(ctx.guild.id): self.messages.pop(ctx.guild.id) self.roles.pop(ctx.guild.id) diff --git a/koala/cogs/base/cog.py b/koala/cogs/base/cog.py index 73c7abb9..5d213f93 100644 --- a/koala/cogs/base/cog.py +++ b/koala/cogs/base/cog.py @@ -12,7 +12,6 @@ # Libs import discord -from discord import app_commands from discord.ext import commands, tasks # Own modules from discord.ext.commands import BadArgument @@ -22,6 +21,7 @@ from . import core from .log import logger from .utils import AUTO_UPDATE_ACTIVITY_DELAY +from ... import env # Constants @@ -36,19 +36,6 @@ def convert_activity_type(argument): raise BadArgument('Unknown activity type %s' % argument) -class BaseCogSlash(commands.Cog, name='Koala'): - """ - Temporary slash command cog. This will be used to get the new discord dev badge ;) - """ - @app_commands.command(name="support", description="KoalaBot Support server link") - async def support(self, interaction: discord.Interaction): - """ - KoalaBot Support server link - :param interaction: - """ - await interaction.response.send_message(core.support_link()) - - class BaseCog(commands.Cog, name='KoalaBot'): """ A discord.py cog with general commands useful to managers of the bot and servers @@ -174,14 +161,6 @@ async def ping(self, ctx): """ await ctx.send(await core.ping(self.bot)) - @commands.command() - async def support(self, ctx): - """ - KoalaBot Support server link - :param ctx: Context of the command - """ - await ctx.send(core.support_link()) - @commands.command(name="clear") @commands.check(koalabot.is_admin) async def clear(self, ctx, amount: int = 1): @@ -250,12 +229,47 @@ async def version(self, ctx): await ctx.send(core.get_version()) +class BaseCogV2(commands.Cog, name="KoalaBot"): + def __init__(self, bot: commands.Bot): + self._bot = bot + + @commands.Cog.listener() + async def on_ready(self): + logger.info("Bot is ready.") + + @commands.command(name="enableExt") + @commands.check(koalabot.is_admin) + async def enable_koala_ext(self, ctx, koala_extension): + """ + /extension + """ + await ctx.send("This command has been moved > `/extension`") + + @commands.command(name="disableExt") + @commands.check(koalabot.is_admin) + async def disable_koala_ext(self, ctx, koala_extension): + """ + /extension + """ + await ctx.send("This command has been moved > `/extension`") + + @commands.command(name="listExt") + @commands.check(koalabot.is_admin) + async def list_koala_ext(self, ctx): + """ + /extension + """ + await ctx.send("This command has been moved > `/extension`") + + async def setup(bot: koalabot) -> None: """ Load this cog to the KoalaBot. :param bot: the bot client for KoalaBot """ - await bot.add_cog(BaseCog(bot)) - await bot.add_cog(BaseCogSlash()) + if env.KB2_ENABLED: + await bot.add_cog(BaseCogV2(bot)) + else: + await bot.add_cog(BaseCog(bot)) logger.info("BaseCog is ready.") diff --git a/koala/cogs/colour_role/cog.py b/koala/cogs/colour_role/cog.py index 08dbdfc4..c9feeb70 100644 --- a/koala/cogs/colour_role/cog.py +++ b/koala/cogs/colour_role/cog.py @@ -23,8 +23,11 @@ from .log import logger from .utils import COLOUR_ROLE_NAMING +# Constants +EXTENSION_ID = "ColourRole" # Variables +is_enabled = koalabot.ext_enabled_func(EXTENSION_ID) def is_allowed_to_change_colour(ctx: commands.Context): @@ -46,21 +49,6 @@ def is_allowed_to_change_colour(ctx: commands.Context): return allowed_set & author_set -def colour_is_enabled(ctx): - """ - A command used to check if the guild has enabled twitch alert - e.g. @commands.check(koalabot.is_admin) - :param ctx: The context of the message - :return: True if admin or test, False otherwise - """ - try: - result = koalabot.check_guild_has_ext(ctx, "ColourRole") - except PermissionError: - result = False - - return result or (str(ctx.author) == koalabot.TEST_USER and koalabot.is_dpytest) - - class ColourRole(commands.Cog): """ A discord.py cog with commands to allow server members to change their display name colours to something of their choosing. @@ -111,7 +99,7 @@ def get_colour_from_hex_str(self, colour_str: str) -> discord.Colour: @commands.cooldown(1, 15, commands.BucketType.member) @commands.check(is_allowed_to_change_colour) - @commands.check(colour_is_enabled) + @commands.check(is_enabled) @commands.command(name="customColour", aliases=["custom_colour", "customColor", "custom_color"]) async def custom_colour(self, ctx: commands.Context, colour_str: str): """ @@ -125,6 +113,8 @@ async def custom_colour(self, ctx: commands.Context, colour_str: str): :param ctx: Context of the command :param colour_str: The colour hex string specified, or "no" in case of cancelling colour """ + koalabot.is_kb2(ctx, EXTENSION_ID) + colour_str = colour_str.upper() if colour_str[0] == "#": colour_str = colour_str[1:] @@ -401,8 +391,8 @@ def role_already_exists(ctx: commands.Context, colour_str: str): guild: discord.Guild = ctx.guild return role_name in [role.name for role in guild.roles] + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(colour_is_enabled) @commands.command(name="listProtectedRoleColours", aliases=["list_protected_role_colours", "listInvalidCustomColours", "listProtectedRoleColors", "listInvalidCustomColors"]) @@ -414,6 +404,8 @@ async def list_protected_role_colours(self, ctx: commands.Context): :param ctx: Context of the command :return: Sends a message with the mentions of the roles that are protected in a guild """ + koalabot.is_kb2(ctx, EXTENSION_ID) + roles = self.get_protected_roles(ctx.guild) # logger.debug(roles) msg = "Roles whose colour is protected are:\r\n" @@ -421,8 +413,8 @@ async def list_protected_role_colours(self, ctx: commands.Context): msg += f"{role.mention}\n" await ctx.send(msg[:-1]) + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(colour_is_enabled) @commands.command(name="listCustomColourAllowedRoles", aliases=["list_custom_colour_allowed_roles"]) async def list_custom_colour_allowed_roles(self, ctx: commands.Context): @@ -433,6 +425,8 @@ async def list_custom_colour_allowed_roles(self, ctx: commands.Context): :param ctx: Context of the command :return: Sends a message with the mentions of the roles that are protected in a guild. """ + koalabot.is_kb2(ctx, EXTENSION_ID) + roles = self.get_custom_colour_allowed_roles(ctx) # logger.debug(roles) msg = "Roles allowed to have a custom colour are:\r\n" @@ -469,8 +463,8 @@ def get_protected_roles(self, guild: discord.Guild) -> List[discord.Role]: roles = [guild.get_role(role_id) for role_id in role_ids] return roles + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(colour_is_enabled) @commands.command(name="addProtectedRoleColour", aliases=["add_protected_role_colour", "addInvalidCustomColourRole", "addInvalidCustomColorRole", "addProtectedRoleColor"]) @@ -482,6 +476,8 @@ async def add_protected_role_colour(self, ctx: commands.Context, *, role_str: st :param ctx: Context of the command :param role_str: The role to add (ID, name or mention) """ + koalabot.is_kb2(ctx, EXTENSION_ID) + role: discord.Role = await commands.RoleConverter().convert(ctx, role_str) if not role: await ctx.send("Please specify a single valid role's mention, ID or name.") @@ -490,8 +486,8 @@ async def add_protected_role_colour(self, ctx: commands.Context, *, role_str: st await ctx.send(f"Added {role.mention} to the list of roles whose colours are protected.") await self.rearrange_custom_colour_role_positions(ctx.guild) + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(colour_is_enabled) @commands.command(name="removeProtectedRoleColour", aliases=["remove_protected_role_colour", "removeProtectedRoleColor", "removeInvalidCustomColourRole", "removeInvalidCustomColorRole"]) @@ -503,6 +499,8 @@ async def remove_protected_role_colour(self, ctx: commands.Context, *, role_str: :param ctx: Context of the command :param role_str: The role to remove (ID, name or mention) """ + koalabot.is_kb2(ctx, EXTENSION_ID) + role: discord.Role = await commands.RoleConverter().convert(ctx, role_str) if not role: await ctx.send("Please specify a single valid role's mention, ID or name.") @@ -511,8 +509,8 @@ async def remove_protected_role_colour(self, ctx: commands.Context, *, role_str: await ctx.send(f"Removed {role.mention} from the list of roles whose colours are protected.") await self.rearrange_custom_colour_role_positions(ctx.guild) + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(colour_is_enabled) @commands.command(name="addCustomColourAllowedRole", aliases=["add_custom_colour_allowed_role", "addCustomColorAllowedRole"]) async def add_custom_colour_allowed_role(self, ctx: commands.Context, *, role_str: str): @@ -523,6 +521,8 @@ async def add_custom_colour_allowed_role(self, ctx: commands.Context, *, role_st :param ctx: Context of the command :param role_str: The role to add (ID, name or mention) """ + koalabot.is_kb2(ctx, EXTENSION_ID) + role: discord.Role = await commands.RoleConverter().convert(ctx, role_str) if not role: await ctx.send("Please specify a single valid role's mention, ID or name.") @@ -530,8 +530,8 @@ async def add_custom_colour_allowed_role(self, ctx: commands.Context, *, role_st self.cr_database_manager.add_colour_change_role_perms(ctx.guild.id, role.id) await ctx.send(f"Added {role.mention} to the list of roles allowed to have a custom colour.") + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(colour_is_enabled) @commands.command(name="removeCustomColourAllowedRole", aliases=["remove_custom_colour_allowed_role", "removeCustomColorAllowedRole"]) async def remove_custom_colour_allowed_role(self, ctx: commands.Context, *, role_str: str): @@ -542,6 +542,8 @@ async def remove_custom_colour_allowed_role(self, ctx: commands.Context, *, role :param ctx: Context of the command :param role_str: The role to remove (ID, name or mention) """ + koalabot.is_kb2(ctx, EXTENSION_ID) + role: discord.Role = await commands.RoleConverter().convert(ctx, role_str) if not role: await ctx.send("Please specify a single valid role's mention, ID or name.") diff --git a/koala/cogs/react_for_role/cog.py b/koala/cogs/react_for_role/cog.py index 16907908..bbe33491 100644 --- a/koala/cogs/react_for_role/cog.py +++ b/koala/cogs/react_for_role/cog.py @@ -23,25 +23,17 @@ from koala.utils import wait_for_message # Own modules from . import core +from .core import get_embed_from_message from .db import get_rfr_message, get_rfr_message_emoji_roles, get_guild_rfr_required_roles, get_guild_rfr_roles, \ get_guild_rfr_messages from .exception import ReactionException, ReactionErrorCode from .log import logger +# Constants +EXTENSION_ID = "ReactForRole"# -def rfr_is_enabled(ctx): - """ - A command used to check if the guild has enabled rfr - e.g. @commands.check(rfr_is_enabled) - :param ctx: The context of the message - :return: True if enabled or test, False otherwise - """ - try: - result = koalabot.check_guild_has_ext(ctx, "ReactForRole") - except PermissionError: - result = False - - return result or (str(ctx.author) == koalabot.TEST_USER and koalabot.is_dpytest) +# Variables +is_enabled = koalabot.ext_enabled_func(EXTENSION_ID) class ReactForRole(commands.Cog): @@ -55,7 +47,7 @@ def __init__(self, bot): @commands.check(koalabot.is_guild_channel) @commands.check(koalabot.is_admin) - @commands.check(rfr_is_enabled) + @commands.check(is_enabled) @commands.group(name="rfr", aliases=["reactForRole", "react_for_role"]) async def react_for_role_group(self, ctx: commands.Context): """ @@ -63,6 +55,7 @@ async def react_for_role_group(self, ctx: commands.Context): :param ctx: Context of the command :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) return @staticmethod @@ -120,8 +113,8 @@ def attachment_img_content_type(mime: Optional[str]): else: return mime.startswith("image/") + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(rfr_is_enabled) @react_for_role_group.command(name="create", aliases=["createMsg", "createMessage"]) async def rfr_create_message(self, ctx: commands.Context): """ @@ -131,6 +124,8 @@ async def rfr_create_message(self, ctx: commands.Context): :param ctx: Context of the command :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + await ctx.send( "Okay, this will create a new react for role message in a channel of your choice." "\nNote: The channel you specify will have its permissions edited to make it such that the @ everyone role " @@ -192,8 +187,8 @@ async def rfr_create_message(self, ctx: commands.Context): "k!rfr subcommands to change the message and add functionality as required.") await del_msg.delete() + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(rfr_is_enabled) @react_for_role_group.command(name="delete", aliases=["deleteMsg", "deleteMessage"]) async def rfr_delete_message(self, ctx: commands.Context): """ @@ -202,6 +197,8 @@ async def rfr_delete_message(self, ctx: commands.Context): :param ctx: Context of the command :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + await ctx.send( "Okay, this will delete an existing react for role message. I'll need some details first though.") msg, channel = await self.get_rfr_message_from_prompts(ctx) @@ -217,8 +214,8 @@ async def rfr_delete_message(self, ctx: commands.Context): async def edit_group(self, ctx: commands.Context): return + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(rfr_is_enabled) @edit_group.command(name="description", aliases=["desc"]) async def rfr_edit_description(self, ctx: commands.Context): """ @@ -227,6 +224,8 @@ async def rfr_edit_description(self, ctx: commands.Context): :param ctx: Context of the command :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + await ctx.send("Okay, this will edit the description of an existing react for role message. I'll need some " "details first though.") msg, channel = await self.get_rfr_message_from_prompts(ctx) @@ -242,8 +241,8 @@ async def rfr_edit_description(self, ctx: commands.Context): else: await ctx.send("Okay, cancelling command.") + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(rfr_is_enabled) @edit_group.command(name="title") async def rfr_edit_title(self, ctx: commands.Context): """ @@ -252,6 +251,8 @@ async def rfr_edit_title(self, ctx: commands.Context): :param ctx: Context of the command :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + await ctx.send("Okay, this will edit the title of an existing react for role message. I'll need some details " "first though.") msg, channel = await self.get_rfr_message_from_prompts(ctx) @@ -267,8 +268,8 @@ async def rfr_edit_title(self, ctx: commands.Context): else: await ctx.send("Okay, cancelling command.") + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(rfr_is_enabled) @edit_group.command(name="thumbnail", aliases=["image", "picture"]) async def rfr_edit_thumbnail(self, ctx: commands.Context): """ @@ -277,6 +278,8 @@ async def rfr_edit_thumbnail(self, ctx: commands.Context): :param ctx: Context of the command :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + await ctx.send("Okay, this will edit the thumbnail of a react for role message. I'll need some details first " "though.") msg, channel = await self.get_rfr_message_from_prompts(ctx) @@ -305,8 +308,8 @@ async def rfr_edit_thumbnail(self, ctx: commands.Context): else: raise commands.BadArgument("Couldn't get an image from the message you sent.") + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(rfr_is_enabled) @edit_group.command(name="inline") async def rfr_edit_inline(self, ctx: commands.Context): """ @@ -316,15 +319,17 @@ async def rfr_edit_inline(self, ctx: commands.Context): :param ctx: Context of the command :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + await ctx.send("Okay, this will change how your embeds look. By default fields are not inline. However, you can" " choose to change this for a specific message or all rfr messages on the server. To do this, I'" "ll need some information though. Can you say if you want a specific message edited, or all mess" "ages on the server?") - input = await self.prompt_for_input(ctx, "all or specific") - if not isinstance(input, str) or not input: + _input = await self.prompt_for_input(ctx, "all or specific") + if not isinstance(_input, str) or not _input: await ctx.send("Okay, cancelling command") else: - input_comm = input.lstrip().rstrip().lower() + input_comm = _input.lstrip().rstrip().lower() if input_comm not in ["all", "specific"]: await ctx.send("Okay, cancelling command.") elif input_comm == "all": @@ -366,13 +371,15 @@ async def rfr_edit_inline(self, ctx: commands.Context): await core.use_inline_rfr_specific(msg, yes_no == "Y") await ctx.send("Okay, should be done. Please check.") + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(rfr_is_enabled) @edit_group.command(name="fixEmbed") async def rfr_fix_embed(self, ctx: commands.Context): """ Cosmetic fix method if the bot ever has a moment and doesn't react with the correct emojis/has duplicates. """ + koalabot.is_kb2(ctx, EXTENSION_ID) + msg, chnl = await self.get_rfr_message_from_prompts(ctx) await core.setup_rfr_reaction_permissions(chnl.guild, chnl, self.bot) emb = core.get_embed_from_message(msg) @@ -410,8 +417,8 @@ async def rfr_fix_embed(self, ctx: commands.Context): await msg.edit(embed=embed) await ctx.send("Tried fixing the message, please check that it's fixed.") + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(rfr_is_enabled) @edit_group.command(name="addRoles") async def rfr_add_roles_to_msg(self, ctx: commands.Context): """ @@ -424,6 +431,8 @@ async def rfr_add_roles_to_msg(self, ctx: commands.Context): :param ctx: Context of the command. :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + await ctx.send( "Okay. This will add roles to an already created react for role message. I'll need some details first " @@ -469,8 +478,8 @@ async def rfr_add_roles_to_msg(self, ctx: commands.Context): if (duplicateRolesFound): await ctx.send("Found duplicate roles in the message, I'm not accepting it.") await ctx.send("Okay, you should see the message with its new emojis now.") + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(rfr_is_enabled) @edit_group.command(name="removeRoles") async def rfr_remove_roles_from_msg(self, ctx: commands.Context): """ @@ -484,6 +493,8 @@ async def rfr_remove_roles_from_msg(self, ctx: commands.Context): :param ctx: Context of the command. :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + await ctx.send( "Okay, this will remove roles from an already existing react for role message. I'll need some details first" " though.") @@ -587,8 +598,8 @@ async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): for x in msg.reactions: await x.remove(payload.member) + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(rfr_is_enabled) @react_for_role_group.command("addRequiredRole") async def rfr_add_guild_required_role(self, ctx: commands.Context, role: discord.Role): """ @@ -599,14 +610,16 @@ async def rfr_add_guild_required_role(self, ctx: commands.Context, role: discord :param role: Role ID/name/mention :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + try: core.add_guild_rfr_required_role(ctx.guild, role.id) await ctx.send(f"Okay, I'll add {role.name} to the list of roles required for RFR usage on the server.") except (commands.CommandError, commands.BadArgument): await ctx.send("Found an issue with your provided argument, couldn't get an actual role. Please try again.") + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(rfr_is_enabled) @react_for_role_group.command("removeRequiredRole") async def rfr_remove_guild_required_role(self, ctx: commands.Context, role: discord.Role): """ @@ -617,6 +630,8 @@ async def rfr_remove_guild_required_role(self, ctx: commands.Context, role: disc :param role: Role ID/name/mention :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + try: core.remove_guild_rfr_required_role(ctx.guild, role.id) await ctx.send( @@ -624,8 +639,8 @@ async def rfr_remove_guild_required_role(self, ctx: commands.Context, role: disc except (commands.CommandError, commands.BadArgument): await ctx.send("Found an issue with your provided argument, couldn't get an actual role. Please try again.") + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(rfr_is_enabled) @react_for_role_group.command("listRequiredRoles") async def rfr_list_guild_required_roles(self, ctx: commands.Context): """ @@ -634,6 +649,8 @@ async def rfr_list_guild_required_roles(self, ctx: commands.Context): :param ctx: Context of the command. :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + role_ids = core.rfr_list_guild_required_roles(ctx.guild).role_ids msg_str = "You will need one of these roles to react to rfr messages on this server:\n" for role_id in role_ids: @@ -730,7 +747,7 @@ async def get_role_member_info(self, emoji_reacted: discord.PartialEmoji, guild_ message: discord.Message = await channel.fetch_message(message_id) if not message: raise ReactionException(ReactionErrorCode.UNKNOWN_MESSAGE_REACTION, message_id, guild_id) - embed: discord.Embed = self.get_embed_from_message(message) + embed: discord.Embed = get_embed_from_message(message) if emoji_reacted.is_unicode_emoji(): # Unicode Emoji rep = emoji.emojize(emoji_reacted.name) diff --git a/koala/cogs/text_filter/cog.py b/koala/cogs/text_filter/cog.py index 879fc951..0f8b1248 100644 --- a/koala/cogs/text_filter/cog.py +++ b/koala/cogs/text_filter/cog.py @@ -21,22 +21,9 @@ from .utils import type_exists, build_word_list_embed, build_moderation_channel_embed, \ create_default_embed, build_moderation_deleted_embed +EXTENSION_ID = "TextFilter" -def text_filter_is_enabled(ctx): - """ - A command used to check if the guild has enabled TextFilter - e.g. @commands.check(koalabot.is_admin) - - :param ctx: The context of the message - :return: True if admin or test, False otherwise - """ - try: - result = koalabot.check_guild_has_ext(ctx, "TextFilter") - except PermissionError: - result = False - - return result or (str(ctx.author) == koalabot.TEST_USER and koalabot.is_dpytest) - +is_enabled = koalabot.ext_enabled_func(EXTENSION_ID) class TextFilter(commands.Cog, name="TextFilter"): """ @@ -49,8 +36,8 @@ def __init__(self, bot): self.tf_database_manager = TextFilterDBManager(bot) @commands.command(name="filter", aliases=["filter_word"]) + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(text_filter_is_enabled) async def filter_new_word(self, ctx, word, filter_type="banned", too_many_arguments=None): """ Adds a new word to the filtered text list @@ -61,6 +48,8 @@ async def filter_new_word(self, ctx, word, filter_type="banned", too_many_argume :param too_many_arguments: Used to check if too many arguments have been given :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + error = """Something has gone wrong, your word may already be filtered or you have entered the command incorrectly. Try again with: `k!filter [filtered_text] [[risky] or [banned]]`""" if too_many_arguments is None and type_exists(filter_type): @@ -69,9 +58,9 @@ async def filter_new_word(self, ctx, word, filter_type="banned", too_many_argume return raise Exception(error) + @commands.check(is_enabled) @commands.command(name="filterRegex", aliases=["filter_regex"]) @commands.check(koalabot.is_admin) - @commands.check(text_filter_is_enabled) async def filter_new_regex(self, ctx, regex, filter_type="banned", too_many_arguments=None): """ Adds a new regex to the filtered text list @@ -82,6 +71,7 @@ async def filter_new_regex(self, ctx, regex, filter_type="banned", too_many_argu :param too_many_arguments: Used to check if too many arguments have been given :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) error = r"""Something has gone wrong, your regex may be invalid, this regex may already be filtered or you have entered the command incorrectly. Try again with: `k!filterRegex [filtered_regex] [[risky] or [banned]]`. One example for a regex could be to block emails @@ -97,8 +87,8 @@ async def filter_new_regex(self, ctx, regex, filter_type="banned", too_many_argu raise Exception(error) @commands.command(name="unfilter", aliases=["unfilter_word"]) + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(text_filter_is_enabled) async def unfilter_word(self, ctx, word, too_many_arguments=None): """ Remove an existing word/test from the filter list @@ -108,6 +98,7 @@ async def unfilter_word(self, ctx, word, too_many_arguments=None): :param too_many_arguments: Used to check if too many arguments have been given :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) error = "Too many arguments, please try again using the following arguments: `k!unfilter [filtered_word]`" if too_many_arguments is None: await self.unfilter_text(ctx, word) @@ -116,8 +107,8 @@ async def unfilter_word(self, ctx, word, too_many_arguments=None): raise Exception(error) @commands.command(name="filterList", aliases=["check_filtered_words", "checkFilteredWords"]) + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(text_filter_is_enabled) async def check_filtered_words(self, ctx): """ Get a list of filtered words on the current guild. @@ -125,14 +116,15 @@ async def check_filtered_words(self, ctx): :param ctx: The discord context :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) all_words_and_types = self.get_list_of_words(ctx) await ctx.channel.send(embed=build_word_list_embed(ctx, all_words_and_types[0], all_words_and_types[1], all_words_and_types[2])) @commands.command(name="modChannelAdd", aliases=["setup_mod_channel", "setupModChannel", "add_mod_channel", "addModChannel"]) + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(text_filter_is_enabled) async def setup_mod_channel(self, ctx, channel_id, too_many_arguments=None): """ Add a mod channel to the current guild @@ -142,6 +134,7 @@ async def setup_mod_channel(self, ctx, channel_id, too_many_arguments=None): :param too_many_arguments: Used to check if too many arguments have been given :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) error = "Channel not found or too many arguments, please try again: `k!setupModChannel [channel_id]`" channel = self.bot.get_channel(int(extract_id(channel_id))) if channel is not None and too_many_arguments is None: @@ -151,8 +144,8 @@ async def setup_mod_channel(self, ctx, channel_id, too_many_arguments=None): raise (Exception(error)) @commands.command(name="modChannelRemove", aliases=["remove_mod_channel", "deleteModChannel", "removeModChannel"]) + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(text_filter_is_enabled) async def remove_mod_channel(self, ctx, channel_id, too_many_arguments=None): """ Remove a mod channel from the guild @@ -162,6 +155,7 @@ async def remove_mod_channel(self, ctx, channel_id, too_many_arguments=None): :param too_many_arguments: Used to check if too many arguments have been given :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) error = """Missing Channel ID or too many arguments remove a mod channel. If you don't know your Channel ID, use `k!listModChannels` to get information on your mod channels.""" channel = self.bot.get_channel(int(extract_id(channel_id))) @@ -172,8 +166,8 @@ async def remove_mod_channel(self, ctx, channel_id, too_many_arguments=None): raise Exception(error) @commands.command(name="modChannelList", aliases=["list_mod_channels", "listModChannels"]) + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(text_filter_is_enabled) async def list_mod_channels(self, ctx): """ Get a list of filtered mod channels in the guild @@ -181,12 +175,13 @@ async def list_mod_channels(self, ctx): :param ctx: The discord context :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) channels = self.tf_database_manager.get_mod_channel(ctx.guild.id) await ctx.channel.send(embed=self.build_channel_list_embed(ctx, channels)) @commands.command(name="ignoreUser") + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(text_filter_is_enabled) async def ignore_user(self, ctx, user, too_many_arguments=None): """ Add a new ignored user to the database @@ -196,6 +191,7 @@ async def ignore_user(self, ctx, user, too_many_arguments=None): :param too_many_arguments: Used to check if too many arguments have been given :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) error = """Missing Ignore ID or too many arguments remove a mod channel. If you don't know your Channel ID, use `k!listModChannels` to get information on your mod channels.""" ignore_id = ctx.message.mentions[0].id @@ -207,8 +203,8 @@ async def ignore_user(self, ctx, user, too_many_arguments=None): raise (Exception(error)) @commands.command(name="ignoreChannel") + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(text_filter_is_enabled) async def ignore_channel(self, ctx, channel: discord.TextChannel, too_many_arguments=None): """ Add a new ignored channel to the database @@ -218,6 +214,7 @@ async def ignore_channel(self, ctx, channel: discord.TextChannel, too_many_argum :param too_many_arguments: Used to check if too many arguments have been given :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) error = """Missing Ignore ID or too many arguments remove a mod channel. If you don't know your Channel ID, use `k!listModChannels` to get information on your mod channels.""" ignore_id = channel.id @@ -229,8 +226,8 @@ async def ignore_channel(self, ctx, channel: discord.TextChannel, too_many_argum raise (Exception(error)) @commands.command(name="unignore", aliases=["remove_ignore", "removeIgnore"]) + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(text_filter_is_enabled) async def remove_ignore(self, ctx, ignore, too_many_arguments=None): """ Remove an ignore from the guild @@ -240,6 +237,7 @@ async def remove_ignore(self, ctx, ignore, too_many_arguments=None): :param too_many_arguments: Used to check if too many arguments have been given :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) if len(ctx.message.mentions) > 0: ignore_id = ctx.message.mentions[0].id elif len(ctx.message.channel_mentions) > 0: @@ -251,8 +249,8 @@ async def remove_ignore(self, ctx, ignore, too_many_arguments=None): return @commands.command(name="ignoreList", aliases=["list_ignored", "listIgnored"]) + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(text_filter_is_enabled) async def list_ignored(self, ctx): """ Get a list all ignored users/channels @@ -260,6 +258,7 @@ async def list_ignored(self, ctx): :param ctx: The discord context :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) ignored = self.tf_database_manager.get_all_ignored(ctx.guild.id) await ctx.channel.send(embed=self.build_ignore_list_embed(ctx, ignored)) @@ -278,6 +277,8 @@ async def on_message(self, message): message.content.startswith(koalabot.OPT_COMMAND_PREFIX + "filter") or \ message.content.startswith(koalabot.OPT_COMMAND_PREFIX + "unfilter"): return + elif koalabot.check_guild_has_ext(None, extension_id=EXTENSION_ID, channel=message.channel, guild_id=message.guild.id): + return elif str(message.channel.type) == 'text' and message.channel.guild is not None: censor_list = self.tf_database_manager.get_filtered_text_for_guild(message.channel.guild.id) for word, filter_type, is_regex in censor_list: diff --git a/koala/cogs/twitch_alert/cog.py b/koala/cogs/twitch_alert/cog.py index 108692d6..3a68e9bc 100644 --- a/koala/cogs/twitch_alert/cog.py +++ b/koala/cogs/twitch_alert/cog.py @@ -28,20 +28,9 @@ # Variables -def twitch_is_enabled(ctx): - """ - A command used to check if the guild has enabled twitch alert - e.g. @commands.check(koalabot.is_admin) - :param ctx: The context of the message - :return: True if admin or test, False otherwise - """ - try: - result = koalabot.check_guild_has_ext(ctx, "TwitchAlert") - except PermissionError: - result = False - - return result +EXTENSION_ID = "TwitchAlert" +is_enabled = koalabot.ext_enabled_func(EXTENSION_ID) class TwitchAlert(commands.Cog): """ @@ -64,13 +53,14 @@ def __init__(self, bot: discord.ext.commands.Bot): self.stop_loop = False @commands.check(koalabot.is_guild_channel) + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(twitch_is_enabled) @commands.group(name="twitch", short_doc="Group of commands for Twitch Alert functionality.") async def twitch_group(self, ctx: commands.Context): """ Group of commands for Twitch Alert functionality. """ + koalabot.is_kb2(ctx, EXTENSION_ID) pass @twitch_group.command(name="editMsg", @@ -83,8 +73,8 @@ async def twitch_group(self, ctx: commands.Context): f"""(e.g. Your favourite stream is now live!) Example: {CP}twitch editMsg #text-channel \"Your favourite stream is now live!\"""")) + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(twitch_is_enabled) async def edit_default_message(self, ctx, channel: discord.TextChannel, *default_live_message): """ Edit the default message put in a Twitch Alert Notification @@ -94,6 +84,8 @@ async def edit_default_message(self, ctx, channel: discord.TextChannel, *default leave empty for program default :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + channel_id = channel.id if not is_channel_in_guild(self.bot, ctx.message.guild.id, channel_id): @@ -130,8 +122,8 @@ async def edit_default_message(self, ctx, channel: discord.TextChannel, *default : The channel to be modified (e.g. #text-channel) Example: {CP}twitch viewMsg #text-channel""") + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(twitch_is_enabled) async def view_default_message(self, ctx, channel: discord.TextChannel): """ Shows the current default message for Twitch Alerts @@ -140,6 +132,8 @@ async def view_default_message(self, ctx, channel: discord.TextChannel): leave empty for program default :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + channel_id = channel.id if not is_channel_in_guild(self.bot, ctx.message.guild.id, channel_id): @@ -168,8 +162,8 @@ async def view_default_message(self, ctx, channel: discord.TextChannel): f"""(e.g. Your favourite streamer is now live!) Example: {CP}twitch add thenuel #text-channel \"Come watch us play games!\"""") + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(twitch_is_enabled) async def add_user_to_twitch_alert(self, ctx, twitch_username, channel: discord.TextChannel, *custom_live_message): """ @@ -180,6 +174,8 @@ async def add_user_to_twitch_alert(self, ctx, twitch_username, :param custom_live_message: the custom live message for this user's alert :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + channel_id = channel.id twitch_username = str.lower(twitch_username) if not re.search(TWITCH_USERNAME_REGEX, twitch_username): @@ -223,8 +219,8 @@ async def add_user_to_twitch_alert(self, ctx, twitch_username, : The channel to be modified (e.g. #text-channel) Example: {CP}twitch remove thenuel #text-channel""") + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(twitch_is_enabled) async def remove_user_from_twitch_alert(self, ctx, twitch_username, channel: discord.TextChannel): """ Removes a user from a Twitch Alert @@ -233,6 +229,7 @@ async def remove_user_from_twitch_alert(self, ctx, twitch_username, channel: dis :param channel: The discord channel ID of the Twitch Alert :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) channel_id = channel.id @@ -260,8 +257,8 @@ async def remove_user_from_twitch_alert(self, ctx, twitch_username, channel: dis f"""(e.g. Your favourite streamer is now live!) Example: {CP}twitch addTeam thenuel #text-channel \"Come watch us play games!\"""") + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(twitch_is_enabled) async def add_team_to_twitch_alert(self, ctx, team_name, channel: discord.TextChannel, *custom_live_message): """ Add a Twitch team to a Twitch Alert @@ -271,6 +268,8 @@ async def add_team_to_twitch_alert(self, ctx, team_name, channel: discord.TextCh :param custom_live_message: the custom live message for this team's alert :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + channel_id = channel.id team_name = str.lower(team_name) @@ -314,8 +313,8 @@ async def add_team_to_twitch_alert(self, ctx, team_name, channel: discord.TextCh : The channel to be modified (e.g. #text-channel) Example: {CP}twitch removeTeam thenuel #text-channel""") + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(twitch_is_enabled) async def remove_team_from_twitch_alert(self, ctx, team_name, channel: discord.TextChannel): """ Removes a team from a Twitch Alert @@ -324,6 +323,7 @@ async def remove_team_from_twitch_alert(self, ctx, team_name, channel: discord.T :param channel: The discord channel ID of the Twitch Alert :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) channel_id = channel.id @@ -348,14 +348,16 @@ async def remove_team_from_twitch_alert(self, ctx, team_name, channel: discord.T : The discord channel (e.g. #text-channel) Example: {CP}twitch list #text-channel""") + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(twitch_is_enabled) async def list_twitch_alert(self, ctx: discord.ext.commands.Context, channel: discord.TextChannel): """ Shows all current TwitchAlert users and teams in a channel :param ctx: :param channel: The discord channel ID of the Twitch Alert """ + koalabot.is_kb2(ctx, EXTENSION_ID) + if not channel: channel = ctx.channel diff --git a/koala/cogs/verification/api.py b/koala/cogs/verification/api.py index 2082dab0..4cf404ff 100644 --- a/koala/cogs/verification/api.py +++ b/koala/cogs/verification/api.py @@ -85,4 +85,4 @@ def setup(bot: Bot): endpoint = VerifyEndpoint(bot) endpoint.register(sub_app) getattr(bot, "koala_web_app").add_subapp('/{}'.format(VERIFY_ENDPOINT), sub_app) - logger.info("Base API is ready.") + logger.info("Verification API is ready.") diff --git a/koala/cogs/verification/cog.py b/koala/cogs/verification/cog.py index 8abf5906..ab7ff900 100644 --- a/koala/cogs/verification/cog.py +++ b/koala/cogs/verification/cog.py @@ -21,24 +21,10 @@ # Constants +EXTENSION_ID = "Verify" # Variables - - -def verify_is_enabled(ctx): - """ - A command used to check if the guild has enabled verify - e.g. @commands.check(verify_is_enabled) - :param ctx: The context of the message - :return: True if enabled or test, False otherwise - """ - try: - result = koalabot.check_guild_has_ext(ctx, "Verify") - except PermissionError: - result = False - - return result or (str(ctx.author) == koalabot.TEST_USER and koalabot.is_dpytest) - +is_enabled = koalabot.ext_enabled_func(EXTENSION_ID) class Verification(commands.Cog, name="Verify"): @@ -62,9 +48,9 @@ async def on_member_join(self, member): """ await core.send_verify_intro_message(member) + @commands.check(is_enabled) @commands.check(koalabot.is_admin) @commands.command(name="verifyAdd", aliases=["addVerification"]) - @commands.check(verify_is_enabled) async def enable_verification(self, ctx: commands.Context, suffix: str, role: discord.Role): """ Set up a role and email pair for KoalaBot to verify users with @@ -73,12 +59,14 @@ async def enable_verification(self, ctx: commands.Context, suffix: str, role: di :param role: the role to give users with that email verified (e.g. @students) :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + await core.add_verify_role(ctx.guild.id, suffix, role.id, self.bot) await ctx.send(f"Verification enabled for {role} for emails ending with `{suffix}`") + @commands.check(is_enabled) @commands.check(koalabot.is_admin) @commands.command(name="verifyRemove", aliases=["removeVerification"]) - @commands.check(verify_is_enabled) async def disable_verification(self, ctx, suffix: str, role: discord.Role): """ Disable an existing verification listener @@ -87,13 +75,15 @@ async def disable_verification(self, ctx, suffix: str, role: discord.Role): :param role: the role paired with the email (e.g. @students) :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + core.remove_verify_role(ctx.guild.id, suffix, role.id) await ctx.send(f"Emails ending with {suffix} no longer give {role}") + @commands.check(is_enabled) @commands.check(koalabot.is_admin) @commands.command(name="verifyBlacklist") - @commands.check(verify_is_enabled) async def blacklist(self, ctx, user: discord.Member, role: discord.Role, suffix: str): """ Blacklist a user from gaining a specified role using a given email @@ -103,12 +93,14 @@ async def blacklist(self, ctx, user: discord.Member, role: discord.Role, suffix: :param suffix: suffix of email to be blacklisted for user :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + await core.blacklist_member(user.id, ctx.guild.id, role.id, suffix, self.bot) await ctx.send(f"{user} will no longer receive {role} upon verifying with this email suffix") + @commands.check(is_enabled) @commands.check(koalabot.is_admin) @commands.command(name="verifyBlacklistRemove") - @commands.check(verify_is_enabled) async def blacklist_remove(self, ctx, user: discord.Member, role: discord.Role, suffix: str): """ Remove a blacklisted user @@ -118,18 +110,22 @@ async def blacklist_remove(self, ctx, user: discord.Member, role: discord.Role, :param suffix: suffix of email to be un-blacklisted for user :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + await core.remove_blacklist_member(user.id, ctx.guild.id, role.id, suffix, self.bot) await ctx.send(f"{user} will now be able to receive {role} upon verifying with this email suffix") + @commands.check(is_enabled) @commands.check(koalabot.is_admin) @commands.command(name="verifyBlacklistList") - @commands.check(verify_is_enabled) async def blacklist_list(self, ctx): """ List the blacklisted user and role mappings :param ctx: context of the discord message :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + embed = discord.Embed(title=f"Current verification blacklist for {ctx.guild.name}") blacklist_map = core.grouped_list_blacklist(ctx.guild.id, self.bot) @@ -199,14 +195,16 @@ async def get_emails(self, ctx, user_id: int): emails = '\n'.join(core.email_verify_list(user_id)) await ctx.send(f"This user has registered with:\n{emails}") + @commands.check(is_enabled) @commands.command(name="verifyList", aliases=["checkVerifications"]) - @commands.check(verify_is_enabled) async def check_verifications(self, ctx): """ List the current verification setup for the server :param ctx: the context of the discord message :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + embed = discord.Embed(title=f"Current verification setup for {ctx.guild.name}") role_dict = core.grouped_list_verify_role(ctx.guild.id, self.bot) @@ -215,9 +213,9 @@ async def check_verifications(self, ctx): await ctx.send(embed=embed) + @commands.check(is_enabled) @commands.check(koalabot.is_admin) @commands.command(name="reVerify") - @commands.check(verify_is_enabled) async def re_verify(self, ctx, role: discord.Role): """ Removes a role from all users who have it and marks them as needing to re-verify before giving it back @@ -225,6 +223,8 @@ async def re_verify(self, ctx, role: discord.Role): :param role: the role to be removed and re-verified (e.g. @students) :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + await core.re_verify_role(ctx.guild.id, role.id, self.bot) await ctx.send("That role has now been removed from all users and they will need to " "re-verify the associated email.") diff --git a/koala/cogs/verification/models.py b/koala/cogs/verification/models.py index a8a7784d..41460f67 100644 --- a/koala/cogs/verification/models.py +++ b/koala/cogs/verification/models.py @@ -7,7 +7,7 @@ class VerifiedEmails: __tablename__ = 'verified_emails' u_id = Column(DiscordSnowflake, primary_key=True) - email = Column(VARCHAR(100, collation="utf8_bin"), primary_key=True) + email = Column(VARCHAR(255, collation="utf8_bin"), primary_key=True) def __repr__(self): return "" % \ @@ -18,7 +18,7 @@ def __repr__(self): class NonVerifiedEmails: __tablename__ = 'non_verified_emails' u_id = Column(DiscordSnowflake) - email = Column(VARCHAR(100)) + email = Column(VARCHAR(255)) token = Column(VARCHAR(8), primary_key=True) def __repr__(self): diff --git a/koala/cogs/voting/cog.py b/koala/cogs/voting/cog.py index 852b5260..522e7d14 100644 --- a/koala/cogs/voting/cog.py +++ b/koala/cogs/voting/cog.py @@ -24,9 +24,10 @@ # Constants +EXTENSION_ID = "Vote" # Variables - +is_enabled = koalabot.ext_enabled_func(EXTENSION_ID) def currently_configuring(): """ @@ -52,21 +53,6 @@ async def predicate(ctx): return commands.check(predicate) -def vote_is_enabled(ctx): - """ - A command used to check if the guild has enabled verify - e.g. @commands.check(vote_is_enabled) - :param ctx: The context of the message - :return: True if enabled or test, False otherwise - """ - try: - result = koalabot.check_guild_has_ext(ctx, "Vote") - except PermissionError: - result = False - - return result or (str(ctx.author) == koalabot.TEST_USER and koalabot.is_dpytest) - - class Voting(commands.Cog, name="Vote"): def __init__(self, bot): """ @@ -152,24 +138,28 @@ async def on_raw_reaction_remove(self, payload): """ await self.update_vote_message(payload.message_id, payload.user_id) + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(vote_is_enabled) @commands.group(name="vote") async def vote(self, ctx): """ Use k!vote create to create a vote! """ + koalabot.is_kb2(ctx, EXTENSION_ID) + if ctx.invoked_subcommand is None: await ctx.send(f"Please use `{koalabot.COMMAND_PREFIX}help vote` for more information") + @commands.check(is_enabled) @commands.check(koalabot.is_admin) - @commands.check(vote_is_enabled) @vote.command(name="create") async def start_vote(self, ctx, *, title): """ Creates a new vote :param title: The title of the vote """ + koalabot.is_kb2(ctx, EXTENSION_ID) + with session_manager() as session: if self.vote_manager.has_active_vote(ctx.author.id): guild_name = self.bot.get_guild(self.vote_manager.get_configuring_vote(ctx.author.id).guild) @@ -189,7 +179,7 @@ async def start_vote(self, ctx, *, title): await ctx.send(f"Vote titled `{title}` created for guild {ctx.guild.name}. Use `{koalabot.COMMAND_PREFIX}help vote` to see how to configure it.") @currently_configuring() - @commands.check(vote_is_enabled) + @commands.check(is_enabled) @vote.command(name="addRole") async def add_role(self, ctx, *, role: discord.Role): """ @@ -197,24 +187,28 @@ async def add_role(self, ctx, *, role: discord.Role): If no roles are added, the vote will go to all users in a guild (unless a target voice channel has been set) :param role: role id (e.g. 135496683009081345) or a role ping (e.g. @Student) """ + koalabot.is_kb2(ctx, EXTENSION_ID) + vote = self.vote_manager.get_configuring_vote(ctx.author.id) vote.add_role(role.id) await ctx.send(f"Vote will be sent to those with the {role.name} role") @currently_configuring() - @commands.check(vote_is_enabled) + @commands.check(is_enabled) @vote.command(name="removeRole") async def remove_role(self, ctx, *, role: discord.Role): """ Removes a role to the list of roles the vote will be sent to :param role: role id (e.g. 135496683009081345) or a role ping (e.g. @Student) """ + koalabot.is_kb2(ctx, EXTENSION_ID) + vote = self.vote_manager.get_configuring_vote(ctx.author.id) vote.remove_role(role.id) await ctx.send(f"Vote will no longer be sent to those with the {role.name} role") @currently_configuring() - @commands.check(vote_is_enabled) + @commands.check(is_enabled) @vote.command(name="setChair") async def set_chair(self, ctx, *, chair: discord.Member = None): """ @@ -222,6 +216,8 @@ async def set_chair(self, ctx, *, chair: discord.Member = None): If no chair defaults to sending the message to the channel the vote is closed in :param chair: user id (e.g. 135496683009081345) or ping (e.g. @ito#8813) """ + koalabot.is_kb2(ctx, EXTENSION_ID) + vote = self.vote_manager.get_configuring_vote(ctx.author.id) if chair: try: @@ -235,7 +231,7 @@ async def set_chair(self, ctx, *, chair: discord.Member = None): await ctx.send(f"Results will be sent to the channel vote is closed in") @currently_configuring() - @commands.check(vote_is_enabled) + @commands.check(is_enabled) @vote.command(name="setChannel") async def set_channel(self, ctx, *, channel: discord.VoiceChannel = None): """ @@ -243,6 +239,8 @@ async def set_channel(self, ctx, *, channel: discord.VoiceChannel = None): If there isn't one set votes will go to all users in a guild (unless target roles have been added) :param channel: channel id (e.g. 135496683009081345) or mention (e.g. #cool-channel) """ + koalabot.is_kb2(ctx, EXTENSION_ID) + vote = self.vote_manager.get_configuring_vote(ctx.author.id) if channel: vote.set_vc(channel.id) @@ -252,7 +250,7 @@ async def set_channel(self, ctx, *, channel: discord.VoiceChannel = None): await ctx.send("Removed channel restriction on vote") @currently_configuring() - @commands.check(vote_is_enabled) + @commands.check(is_enabled) @vote.command(name="addOption") async def add_option(self, ctx, *, option_string): """ @@ -260,6 +258,8 @@ async def add_option(self, ctx, *, option_string): separate the title and description with a "+" e.g. option title+option description :param option_string: a title and description for the option separated by a '+' """ + koalabot.is_kb2(ctx, EXTENSION_ID) + vote = self.vote_manager.get_configuring_vote(ctx.author.id) if len(vote.options) > 9: await ctx.send("Vote has maximum number of options already (10)") @@ -276,19 +276,21 @@ async def add_option(self, ctx, *, option_string): await ctx.send(f"Option {header} with description {body} added to vote") @currently_configuring() - @commands.check(vote_is_enabled) + @commands.check(is_enabled) @vote.command(name="removeOption") async def remove_option(self, ctx, index: int): """ Removes an option from a vote based on it's index :param index: the number of the option """ + koalabot.is_kb2(ctx, EXTENSION_ID) + vote = self.vote_manager.get_configuring_vote(ctx.author.id) vote.remove_option(index) await ctx.send(f"Option number {index} removed") @currently_configuring() - @commands.check(vote_is_enabled) + @commands.check(is_enabled) @vote.command(name="setEndTime") async def set_end_time(self, ctx, *, time_string): """ @@ -297,6 +299,8 @@ async def set_end_time(self, ctx, *, time_string): :param time_string: string representing a time e.g. "2021-03-22 12:56" or "tomorrow at 10am" or "in 5 days and 15 minutes" :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + now = time.time() vote = self.vote_manager.get_configuring_vote(ctx.author.id) cal = parsedatetime.Calendar() @@ -312,24 +316,28 @@ async def set_end_time(self, ctx, *, time_string): await ctx.send(f"Vote set to end at {time.strftime('%Y-%m-%d %H:%M:%S', end_time_readable)} UTC") @currently_configuring() - @commands.check(vote_is_enabled) + @commands.check(is_enabled) @vote.command(name="preview") async def preview_vote(self, ctx): """ Generates a preview of what users will see with the current configuration of the vote """ + koalabot.is_kb2(ctx, EXTENSION_ID) + vote = self.vote_manager.get_configuring_vote(ctx.author.id) msg = await ctx.send(embed=create_embed(vote)) await add_reactions(vote, msg) - @commands.check(vote_is_enabled) @has_current_votes() + @commands.check(is_enabled) @vote.command(name="cancel") async def cancel_vote(self, ctx, *, title): """ Cancels a vote you are setting up or have sent :param title: title of the vote to cancel """ + koalabot.is_kb2(ctx, EXTENSION_ID) + with session_manager() as session: v_id = self.vote_manager.vote_lookup[(ctx.author.id, title)] if v_id in self.vote_manager.sent_votes.keys(): @@ -338,14 +346,16 @@ async def cancel_vote(self, ctx, *, title): self.vote_manager.cancel_configuring_vote(ctx.author.id, session=session) await ctx.send(f"Vote {title} has been cancelled.") - @commands.check(vote_is_enabled) @has_current_votes() + @commands.check(is_enabled) @vote.command("list", aliases=["currentVotes"]) async def check_current_votes(self, ctx): """ Return a list of all votes you have in this guild. :return: """ + koalabot.is_kb2(ctx, EXTENSION_ID) + with session_manager() as session: embed = discord.Embed(title="Your current votes") votes = session.execute(select(Votes.title).filter_by(author_id=ctx.author.id, guild_id=ctx.guild.id)).all() @@ -356,6 +366,7 @@ async def check_current_votes(self, ctx): await ctx.send(embed=embed) @currently_configuring() + @commands.check(is_enabled) @vote.command(name="send") async def send_vote(self, ctx): """ @@ -390,13 +401,15 @@ async def send_vote(self, ctx): logger.error(f"tried to send vote to user {user.id} but direct messages are turned off.") await ctx.send(f"Sent vote to {len(users)} users") - @commands.check(vote_is_enabled) @has_current_votes() + @commands.check(is_enabled) @vote.command(name="close") async def close(self, ctx, *, title): """ Ends a vote, and collects the results """ + koalabot.is_kb2(ctx, EXTENSION_ID) + vote_id = self.vote_manager.vote_lookup.get((ctx.author.id, title)) if vote_id not in self.vote_manager.sent_votes.keys(): if ctx.author.id in self.vote_manager.configuring_votes.keys(): @@ -421,13 +434,14 @@ async def close(self, ctx, *, title): else: await ctx.send(embed=embed) - @commands.check(vote_is_enabled) @has_current_votes() @vote.command(name="checkResults") async def check_results(self, ctx, *, title): """ Checks the results of a vote without closing it """ + koalabot.is_kb2(ctx, EXTENSION_ID) + vote_id = self.vote_manager.vote_lookup.get((ctx.author.id, title)) if vote_id is None: raise ValueError(f"{title} is not a valid vote title for user {ctx.author.name}") diff --git a/koala/env.py b/koala/env.py index 5201dd3a..e27c7970 100644 --- a/koala/env.py +++ b/koala/env.py @@ -47,3 +47,7 @@ DB_URL = f"sqlite+pysqlcipher://:x'{DB_KEY}'@/{SQLITE_DB_PATH.absolute()}?charset=utf8mb4" else: DB_URL = f"sqlite:///{SQLITE_DB_PATH.absolute()}?charset=utf8mb4" + +ENV_PREFIX = os.environ.get("ENV_PREFIX", "") + +KB2_ENABLED = eval(os.environ.get("KB2_ENABLED", "True")) \ No newline at end of file diff --git a/koala/kb2/__init__.py b/koala/kb2/__init__.py new file mode 100644 index 00000000..f075e354 --- /dev/null +++ b/koala/kb2/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import adapter diff --git a/koala/kb2/adapter.py b/koala/kb2/adapter.py new file mode 100644 index 00000000..15496f37 --- /dev/null +++ b/koala/kb2/adapter.py @@ -0,0 +1,74 @@ +import asyncio +import threading +import time +from http.client import OK + +import requests + +from koala.kb2 import env +from koala.kb2.log import logger + + +class AutherOAuthToken: + """ + Auther OAuth Token Manager + https://github.com/JayDwee/auther + """ + _token: str + expires_at: int = 0 + + def request_token(self): + """ + Request OAuth2 JWT Token from Auther + """ + logger.debug("Auther Token Request") + response = requests.post(env.AUTHER_URL + "/token", + data={"grant_type": "client_credentials", "scope": "owner"}, + auth=(env.AUTHER_CLIENT_ID, env.AUTHER_CLIENT_SECRET)) + response_json = response.json() + logger.debug("Auther Token Response: %s", response_json) + self._token = response_json.get("access_token") + self.expires_at = response_json.get("expires_in") + int(time.time()) + + def get_headers(self) -> dict: + return {"Authorization": f"Bearer {self.token}"} + + @property + def token(self) -> str: + """ + Get OAuth2 JWT Token + If expired, requests a new one + + :return: OAuth2 JWT Token + :rtype: str + """ + if time.time() > self.expires_at: + self.request_token() + return self._token + + +class KB2Adapter: + """ + Koala Bot v2 (KB2) Adapter + https://github.com/KoalaBotUK/KB2 + """ + token: AutherOAuthToken = AutherOAuthToken() + + async def on_raw_interaction(self, raw_interaction: dict): + """ + Handle a raw interaction from Discord. + + :param raw_interaction: The raw interaction data from Discord. + :type raw_interaction: dict + """ + try: + logger.debug("Interact New Interaction: %s", raw_interaction) + requests.post(env.KB2_URL + '/gateway-interactions', + json=raw_interaction, + headers={"X-INTERACT-TS": str(int(time.time() * 1000)), + "Content-Type": "application/json"} | self.token.get_headers()) + except Exception as e: + logger.error("Interact Failed to Process: %s", e, exc_info=True) + + +kb2_adapter = KB2Adapter() diff --git a/koala/kb2/env.py b/koala/kb2/env.py new file mode 100644 index 00000000..10c2912e --- /dev/null +++ b/koala/kb2/env.py @@ -0,0 +1,11 @@ +import os + +from dotenv import load_dotenv + +load_dotenv() + +AUTHER_URL = os.environ.get('AUTHER_URL') +AUTHER_CLIENT_ID = os.environ.get('AUTHER_CLIENT_ID') +AUTHER_CLIENT_SECRET = os.environ.get('AUTHER_CLIENT_SECRET') + +KB2_URL = os.environ.get('KB2_URL') diff --git a/koala/kb2/log.py b/koala/kb2/log.py new file mode 100644 index 00000000..38a00197 --- /dev/null +++ b/koala/kb2/log.py @@ -0,0 +1,3 @@ +from koala.log import get_logger + +logger = get_logger(__name__) diff --git a/koala/kb2/models.py b/koala/kb2/models.py new file mode 100644 index 00000000..1b0d0178 --- /dev/null +++ b/koala/kb2/models.py @@ -0,0 +1,34 @@ +from typing import List + +from pynamodb.attributes import NumberAttribute, BooleanAttribute, UnicodeAttribute, MapAttribute, ListAttribute +from pynamodb.models import Model + +from koala import env + +error_versions = {} + + +class ExtensionAttr(MapAttribute): + id: str = UnicodeAttribute(hash_key=True) + version: int = NumberAttribute() + enabled: bool = BooleanAttribute() + + +class Guild(Model): + class Meta: + table_name = f'{env.ENV_PREFIX}kb_guilds' + region = 'eu-west-2' + + guild_id: str = UnicodeAttribute(hash_key=True) + extensions: List[ExtensionAttr] = ListAttribute(of=ExtensionAttr, default=list) + + +map_ext = { + "Announce": "announce", + "ColourRole": "colour_role", + "ReactForRole": "rfr", + "TextFilter": "filter", + "TwitchAlert": "twitch_alert", + "Verify": "verify", + "Vote": "vote" +} diff --git a/koala/log.py b/koala/log.py index b724a060..5e6805f9 100644 --- a/koala/log.py +++ b/koala/log.py @@ -18,14 +18,14 @@ def _get_default_warn_log(): - koala_log = logging.FileHandler(filename=Path(_LOG_DIR, "KoalaBotWarn.log")) + koala_log = logging.FileHandler(filename=Path(_LOG_DIR, "KoalaBotWarn.log"), encoding="utf-8") koala_log.setFormatter(_FORMATTER) koala_log.setLevel(logging.WARN) return koala_log def _get_file_handler(log_name, log_level): - file_handler = logging.FileHandler(filename=Path(_LOG_DIR, log_name)) + file_handler = logging.FileHandler(filename=Path(_LOG_DIR, log_name), encoding="utf-8") file_handler.setFormatter(_FORMATTER) file_handler.setLevel(log_level) return file_handler diff --git a/koalabot.py b/koalabot.py index 143731ce..37e62bed 100644 --- a/koalabot.py +++ b/koalabot.py @@ -18,18 +18,20 @@ # Built-in/Generic Imports import asyncio import time +from typing import Any +import aiohttp_cors import discord # Libs from aiohttp import web -import aiohttp_cors from discord.ext import commands from koala import env # Own modules -from koala.db import extension_enabled from koala.env import BOT_TOKEN, BOT_OWNER, API_PORT from koala.errors import KoalaException +from koala.kb2.adapter import kb2_adapter +from koala.kb2.models import Guild, map_ext from koala.log import logger from koala.utils import error_embed @@ -56,17 +58,22 @@ intent.message_content = True is_dpytest = False +check_error: [int, str] = {} + class KoalaBot(commands.Bot): """ The commands.Bot subclass for Koala """ - async def setup_hook(self) -> None: - """ - To perform asynchronous setup after the bot is logged in but before it has connected to the Websocket. - """ - logger.debug("hook setup") - await self.tree.sync() + def parse_interaction_create(self, data: dict) -> None: + self.dispatch('raw_interaction', data) + + async def on_raw_interaction(self, raw_interaction: dict): + await kb2_adapter.on_raw_interaction(raw_interaction) + + async def on_error(self, event_method: str, /, *args: Any, **kwargs: Any) -> None: + logger.error("Error in %s: %s", event_method, args[0], exc_info=args[0]) + async def on_command_error(self, ctx, error: Exception): if ctx.guild is None: @@ -100,6 +107,18 @@ async def on_command_error(self, ctx, error: Exception): description=f"An unexpected error occurred, please contact an administrator Timestamp: {time.time()}")) # FIXME: better timestamp raise error + async def setup_hook(self) -> None: + """ + To perform asynchronous setup after the bot is logged in but before it has connected to the Websocket. + """ + logger.debug("hook setup started") + # await self.tree.sync() + # kb2.main.client.sync_commands() + # kb2.main.client.sync_commands(guild_ids=[g.id for g in kb2.main.client.guilds]) + + self._connection.parsers['INTERACTION_CREATE'] = self.parse_interaction_create + + logger.debug("hook setup complete") def is_owner(ctx: commands.Context): """ @@ -130,8 +149,10 @@ def is_admin(ctx): return ctx.author.guild_permissions.administrator or is_dpytest -def is_dm_channel(ctx): - return isinstance(ctx.channel, discord.channel.DMChannel) +def is_dm_channel(ctx=None, channel=None): + if ctx is not None: + channel = ctx.channel + return isinstance(channel, discord.channel.DMChannel) def is_guild_channel(ctx): @@ -169,18 +190,44 @@ async def dm_group_message(members: [discord.Member], message: str): return count -def check_guild_has_ext(ctx, extension_id): +def check_guild_has_ext(ctx=None, extension_id=None, channel=None, guild_id=None): """ A check for if a guild has a given koala extension :param ctx: A discord context :param extension_id: The koala extension ID :return: True if has ext """ - if is_dm_channel(ctx): - return False - if (not extension_enabled(ctx.message.guild.id, extension_id)) and (not is_dpytest): - raise PermissionError(PERMISSION_ERROR_TEXT) - return True + if channel is None and ctx is not None: + channel = ctx.channel + if guild_id is None and ctx is not None and ctx.guild is not None: + guild_id = ctx.guild.id + + if is_dpytest: + return None + if is_dm_channel(channel=channel): + return "DM Channels don't support this command" + + ext = next((e for e in Guild.get(str(guild_id)).extensions + if e.id == map_ext[extension_id] and e.enabled), + None) + if ext is None: + return "This guild doesn't have this extension enabled" + else: + return None + + +def ext_enabled_func(extension_id): + return lambda ctx: check_guild_has_ext(ctx, extension_id) is None + + +def is_kb2(ctx, extension_id): + if is_dpytest: + return + ext = next((e for e in Guild.get(str(ctx.guild.id)).extensions + if e.id == map_ext[extension_id] and e.enabled), + None) + if ext.version == 2: + raise PermissionError("Please use the new slash commands\n> type `/`") async def run_bot(): diff --git a/requirements.txt b/requirements.txt index b8b8ff28..459114fc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,12 +27,15 @@ pytest-asyncio==0.20.3 pytest-env==0.8.1 pytest-order==1.1.0 python-dotenv==1.0.0 -requests==2.28.2 +requests==2.31.0 six==1.16.0 sqlalchemy==1.4.37 toml==0.10.2 twitchAPI==3.9.0 -urllib3==1.26.14 +urllib3~=1.24 wcwidth==0.2.6 websockets==10.4 yarl==1.8.2 + +discord-interactions~=0.4.0 +pynamodb~=6.0 diff --git a/tests/conftest.py b/tests/conftest.py index 98d28a7a..85f4fdf3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,15 +6,11 @@ # Built-in/Generic Imports import shutil -import discord -import discord.ext.test as dpytest # Libs import pytest import pytest_asyncio -import koala.db as db # Own modules -import koalabot from koala.db import session_manager from tests.log import logger @@ -37,29 +33,6 @@ def teardown_config(): shutil.rmtree(CONFIG_PATH, ignore_errors=True) -@pytest_asyncio.fixture -async def bot(): - import koalabot - intents = discord.Intents.default() - intents.members = True - intents.guilds = True - intents.messages = True - intents.message_content = True - b = koalabot.KoalaBot(koalabot.COMMAND_PREFIX, intents=intents) - await b._async_setup_hook() - await dpytest.empty_queue() - dpytest.configure(b) - return b - - -@pytest.fixture(autouse=True) -def setup_is_dpytest(): - db.__create_sqlite_tables() - koalabot.is_dpytest = True - yield - koalabot.is_dpytest = False - - @pytest_asyncio.fixture async def session(): with session_manager() as session: diff --git a/tests/cogs/__init__.py b/tests/koala/__init__.py similarity index 100% rename from tests/cogs/__init__.py rename to tests/koala/__init__.py diff --git a/tests/cogs/announce/__init__.py b/tests/koala/cogs/__init__.py similarity index 100% rename from tests/cogs/announce/__init__.py rename to tests/koala/cogs/__init__.py diff --git a/tests/cogs/base/__init__.py b/tests/koala/cogs/announce/__init__.py similarity index 100% rename from tests/cogs/base/__init__.py rename to tests/koala/cogs/announce/__init__.py diff --git a/tests/cogs/announce/test_cog.py b/tests/koala/cogs/announce/test_cog.py similarity index 99% rename from tests/cogs/announce/test_cog.py rename to tests/koala/cogs/announce/test_cog.py index 25fa7dab..169ddc00 100644 --- a/tests/cogs/announce/test_cog.py +++ b/tests/koala/cogs/announce/test_cog.py @@ -12,7 +12,7 @@ import koalabot from koala.cogs import announce from tests.log import logger -from tests.tests_utils.last_ctx_cog import LastCtxCog +from tests.koala.tests_utils.last_ctx_cog import LastCtxCog @pytest_asyncio.fixture(autouse=True) diff --git a/tests/cogs/colour_role/__init__.py b/tests/koala/cogs/base/__init__.py similarity index 100% rename from tests/cogs/colour_role/__init__.py rename to tests/koala/cogs/base/__init__.py diff --git a/tests/cogs/base/conftest.py b/tests/koala/cogs/base/conftest.py similarity index 100% rename from tests/cogs/base/conftest.py rename to tests/koala/cogs/base/conftest.py diff --git a/tests/cogs/base/test_api.py b/tests/koala/cogs/base/test_api.py similarity index 96% rename from tests/cogs/base/test_api.py rename to tests/koala/cogs/base/test_api.py index 8b19e4cf..b30b4b34 100644 --- a/tests/cogs/base/test_api.py +++ b/tests/koala/cogs/base/test_api.py @@ -1,448 +1,448 @@ -from http.client import BAD_REQUEST, CREATED, OK - -# Libs -import discord -import discord.ext.test as dpytest -import pytest -from aiohttp import web -from mock import mock - -import koalabot -from koala.cogs.base.api import BaseEndpoint - - -@pytest.fixture -def api_client(bot: discord.ext.commands.Bot, aiohttp_client, loop): - app = web.Application() - endpoint = BaseEndpoint(bot) - app = endpoint.register(app) - return loop.run_until_complete(aiohttp_client(app)) - - -''' - -GET /activity - -''' - - -async def test_get_activities(api_client): - resp = await api_client.get('/scheduled-activity?show_all=False') - assert resp.status == OK - text = await resp.text() - assert text == '[]' - - -async def test_get_activities_bad_param(api_client): - resp = await api_client.get('/scheduled-activity?invalid_arg=abc') - assert resp.status == BAD_REQUEST - - -async def test_get_activities_missing_param(api_client): - resp = await api_client.get('/scheduled-activity') - assert resp.status == BAD_REQUEST - - -''' - -PUT /scheduled-activity - -''' - - -async def test_put_schedule_activity(api_client): - resp = await api_client.put('/scheduled-activity', json= - { - 'activity_type': 'playing', - 'message': 'test', - 'url': 'test.com', - 'start_time': '2025-01-01 00:00:00', - 'end_time': '2026-01-01 00:00:00' - }) - assert resp.status == CREATED - text = await resp.text() - assert text == '{"message": "Activity scheduled"}' - - -async def test_put_schedule_activity_missing_param(api_client): - resp = await api_client.put('/scheduled-activity', json= - { - 'activity_type': 'playing', - 'message': 'test', - 'url': 'test.com', - 'start_time': '2025-01-01 00:00:00' - }) - assert resp.status == BAD_REQUEST - text = (await resp.json())['message'] - assert text == "Unsatisfied Arguments: {'end_time'}" - - -async def test_put_schedule_activity_bad_activity(api_client): - resp = await api_client.put('/scheduled-activity', json= - { - 'activity_type': 'invalidActivity', - 'message': 'test', - 'url': 'test.com', - 'start_time': '2025-01-01 00:00:00', - 'end_time': '2026-01-01 00:00:00' - }) - assert resp.status == BAD_REQUEST - assert (await resp.json())['message'] == 'Error scheduling activity: Invalid activity type' - - -async def test_put_schedule_activity_bad_start_time(api_client): - resp = await api_client.put('/scheduled-activity', json= - { - 'activity_type': 'playing', - 'message': 'test', - 'url': 'test.com', - 'start_time': 'invalid_time', - 'end_time': '2026-01-01 00:00:00' - }) - assert resp.status == BAD_REQUEST - assert (await resp.json())['message'] == 'Error scheduling activity: Bad start / end time' - - -async def test_put_schedule_activity_bad_end_time(api_client): - resp = await api_client.put('/scheduled-activity', json= - { - 'activity_type': 'invalidActivity', - 'message': 'test', - 'url': 'test.com', - 'start_time': '2026-01-01 00:00:00', - 'end_time': 'invalidTime' - }) - assert resp.status == BAD_REQUEST - assert (await resp.json())['message'] == 'Error scheduling activity: Bad start / end time' - - -''' - -PUT /activity - -''' - - -async def test_put_set_activity(api_client): - resp = await api_client.put('/activity', json= - { - 'activity_type': 'playing', - 'name': 'test', - 'url': 'test.com' - }) - assert resp.status == CREATED - text = await resp.text() - assert text == '{"message": "Activity set"}' - assert dpytest.verify().activity().matches( - discord.Activity(type=discord.ActivityType.playing, name="test", url="test.com")) - - -async def test_put_set_activity_bad_req(api_client): - resp = await api_client.put('/activity', json= - { - 'activity_type': 'invalidActivity', - 'name': 'test', - 'url': 'test.com' - }) - assert resp.status == BAD_REQUEST - assert (await resp.json())['message'] == 'Error setting activity: Invalid activity type' - - -async def test_put_set_activity_missing_param(api_client): - resp = await api_client.put('/activity', json= - { - 'activity_type': 'invalidActivity', - 'url': 'test.com' - }) - assert resp.status == BAD_REQUEST - assert (await resp.json())['message'] == "Unsatisfied Arguments: {'name'}" - - -''' - -GET /ping - -''' - - -async def test_get_ping(api_client): - with mock.patch('discord.client.Client.latency', new_callable=mock.PropertyMock) as mock_last_transaction: - mock_last_transaction.return_value = 0.42 - resp = await api_client.get('/ping') - assert resp.status == OK - text = (await resp.json())['message'] - assert "Pong! 420ms" in text - - -''' - -GET /version - -''' - - -async def test_get_version(api_client): - resp = await api_client.get('/version') - text = (await resp.json())['message'] - assert f"version: {koalabot.__version__}" in text - - -''' - -GET /support - -''' - - -async def test_get_support_link(api_client): - resp = await api_client.get('/support') - text = (await resp.json())['message'] - assert "Join our support server for more help! https://discord.gg/5etEjVd" in text - - -''' - -POST /load-cog - -''' - - -async def test_post_load_cog(api_client): - resp = await api_client.post('/load-cog', json= - { - 'extension': 'announce', - 'package': koalabot.COGS_PACKAGE - }) - assert resp.status == OK - text = (await resp.json())['message'] - assert text == "Cog loaded" - - -async def test_post_load_base_cog(api_client): - resp = await api_client.post('/load-cog', json= - { - 'extension': 'base', - 'package': koalabot.COGS_PACKAGE - }) - assert resp.status == OK - text = (await resp.json())['message'] - assert text == "Cog loaded" - - -async def test_post_load_cog_bad_req(api_client): - resp = await api_client.post('/load-cog', json= - { - 'extension': 'invalidCog', - 'package': koalabot.COGS_PACKAGE - }) - assert resp.status == BAD_REQUEST - assert (await resp.json())['message'] == 'Error loading cog: Invalid extension' - - -async def test_post_load_cog_missing_param(api_client): - resp = await api_client.post('/load-cog', json={}) - assert resp.status == BAD_REQUEST - assert (await resp.json())['message'] == "Unsatisfied Arguments: {'extension'}" - - -async def test_post_load_cog_already_loaded(api_client): - await api_client.post('/load-cog', json= - { - 'extension': 'announce', - 'package': koalabot.COGS_PACKAGE - }) - - resp = await api_client.post('/load-cog', json= - { - 'extension': 'announce', - 'package': koalabot.COGS_PACKAGE - }) - assert resp.status == BAD_REQUEST - assert (await resp.json())['message'] == 'Error loading cog: Already loaded' - - -''' - -POST /unload-cog - -''' - - -async def test_post_unload_cog(api_client): - await api_client.post('/load-cog', json= - { - 'extension': 'announce', - 'package': koalabot.COGS_PACKAGE - }) - - resp = await api_client.post('/unload-cog', json= - { - 'extension': 'announce', - 'package': koalabot.COGS_PACKAGE - }) - assert resp.status == OK - text = (await resp.json())['message'] - assert text == "Cog unloaded" - - -async def test_post_unload_cog_not_loaded(api_client): - resp = await api_client.post('/unload-cog', json= - { - 'extension': 'announce', - 'package': koalabot.COGS_PACKAGE - }) - assert resp.status == BAD_REQUEST - assert (await resp.json())['message'] == 'Error unloading cog: Extension not loaded' - - -async def test_post_unload_cog_missing_param(api_client): - resp = await api_client.post('/unload-cog', json={}) - assert resp.status == BAD_REQUEST - assert (await resp.json())['message'] == "Unsatisfied Arguments: {'extension'}" - - -async def test_post_unload_base_cog(api_client): - resp = await api_client.post('/unload-cog', json= - { - 'extension': 'BaseCog', - 'package': koalabot.COGS_PACKAGE - }) - assert resp.status == BAD_REQUEST - text = (await resp.json())['message'] - assert text == "Error unloading cog: Sorry, you can't unload the base cog" - - -''' - -POST /enable-extension - -''' - - -@mock.patch("koalabot.ENABLED_COGS", ["announce"]) -async def test_post_enable_extension(api_client, bot): - await koalabot.load_all_cogs(bot) - guild: discord.Guild = dpytest.get_config().guilds[0] - resp = await api_client.post('/enable-extension', json={ - 'guild_id': guild.id, - 'koala_ext': 'Announce' - }) - - assert resp.status == OK - text = (await resp.json())['message'] - assert text == "Extension enabled" - - -async def test_post_enable_extension_bad_req(api_client): - guild: discord.Guild = dpytest.get_config().guilds[0] - - resp = await api_client.post('/enable-extension', json= - { - 'guild_id': guild.id, - 'koala_ext': 'Invalid Extension' - }) - assert resp.status == BAD_REQUEST - text = (await resp.json())['message'] - assert text == "Error enabling extension: Invalid extension" - - -async def test_post_enable_extension_missing_param(api_client): - guild: discord.Guild = dpytest.get_config().guilds[0] - resp = await api_client.post('/enable-extension', json= - { - 'guild_id': guild.id - }) - assert resp.status == BAD_REQUEST - text = (await resp.json())['message'] - assert text == "Unsatisfied Arguments: {'koala_ext'}" - - -''' - -POST /disable-extension - -''' - - -@mock.patch("koalabot.ENABLED_COGS", ['announce']) -async def test_post_disable_extension(api_client, bot): - await koalabot.load_all_cogs(bot) - guild: discord.Guild = dpytest.get_config().guilds[0] - setup = await api_client.post('/enable-extension', json={ - 'guild_id': guild.id, - 'koala_ext': 'Announce' - }) - assert setup.status == OK - - resp = await api_client.post('/disable-extension', json={ - 'guild_id': guild.id, - 'koala_ext': 'Announce' - }) - assert resp.status == OK - text = await resp.text() - assert text == '{"message": "Extension disabled"}' - - -async def test_post_disable_extension_not_enabled(api_client): - guild: discord.Guild = dpytest.get_config().guilds[0] - resp = await api_client.post('/disable-extension', json={ - 'guild_id': guild.id, - 'koala_ext': 'Announce' - }) - assert resp.status == BAD_REQUEST - text = (await resp.json())['message'] - assert text == "Error disabling extension: Extension not enabled" - - -async def test_post_disable_extension_missing_param(api_client): - guild: discord.Guild = dpytest.get_config().guilds[0] - resp = await api_client.post('/disable-extension', json={ - 'guild_id': guild.id - }) - assert resp.status == BAD_REQUEST - text = (await resp.json())['message'] - assert text == "Unsatisfied Arguments: {'koala_ext'}" - - -async def test_post_disable_extension_bad_req(api_client): - guild: discord.Guild = dpytest.get_config().guilds[0] - - resp = await api_client.post('/disable-extension', json= - { - 'guild_id': guild.id, - 'koala_ext': 'Invalid Extension' - }) - assert resp.status == BAD_REQUEST - text = (await resp.json())['message'] - assert text == "Error disabling extension: Extension not enabled" - - -''' - -GET /extensions - -''' - - -@mock.patch("koalabot.ENABLED_COGS", ['announce']) -async def test_get_extension(api_client, bot): - await koalabot.load_all_cogs(bot) - guild: discord.Guild = dpytest.get_config().guilds[0] - resp = await api_client.get('/extensions?guild_id={}'.format(guild.id)) - assert resp.status == OK - text = await resp.text() - assert text == '["Announce"]' - - -async def test_get_extension_bad_param(api_client): - resp = await api_client.get('/extensions?invalid-arg=abc') - assert resp.status == BAD_REQUEST - text = (await resp.json())['message'] - assert text == "Unsatisfied Arguments: {'guild_id'}" - - -async def test_get_extension_missing_param(api_client): - resp = await api_client.get('/extensions') - assert resp.status == BAD_REQUEST - text = (await resp.json())['message'] - assert text == "Unsatisfied Arguments: {'guild_id'}" +from http.client import BAD_REQUEST, CREATED, OK + +# Libs +import discord +import discord.ext.test as dpytest +import pytest +from aiohttp import web +from mock import mock + +import koalabot +from koala.cogs.base.api import BaseEndpoint + + +@pytest.fixture +def api_client(bot: discord.ext.commands.Bot, aiohttp_client, loop): + app = web.Application() + endpoint = BaseEndpoint(bot) + app = endpoint.register(app) + return loop.run_until_complete(aiohttp_client(app)) + + +''' + +GET /activity + +''' + + +async def test_get_activities(api_client): + resp = await api_client.get('/scheduled-activity?show_all=False') + assert resp.status == OK + text = await resp.text() + assert text == '[]' + + +async def test_get_activities_bad_param(api_client): + resp = await api_client.get('/scheduled-activity?invalid_arg=abc') + assert resp.status == BAD_REQUEST + + +async def test_get_activities_missing_param(api_client): + resp = await api_client.get('/scheduled-activity') + assert resp.status == BAD_REQUEST + + +''' + +PUT /scheduled-activity + +''' + + +async def test_put_schedule_activity(api_client): + resp = await api_client.put('/scheduled-activity', json= + { + 'activity_type': 'playing', + 'message': 'test', + 'url': 'test.com', + 'start_time': '2025-01-01 00:00:00', + 'end_time': '2026-01-01 00:00:00' + }) + assert resp.status == CREATED + text = await resp.text() + assert text == '{"message": "Activity scheduled"}' + + +async def test_put_schedule_activity_missing_param(api_client): + resp = await api_client.put('/scheduled-activity', json= + { + 'activity_type': 'playing', + 'message': 'test', + 'url': 'test.com', + 'start_time': '2025-01-01 00:00:00' + }) + assert resp.status == BAD_REQUEST + text = (await resp.json())['message'] + assert text == "Unsatisfied Arguments: {'end_time'}" + + +async def test_put_schedule_activity_bad_activity(api_client): + resp = await api_client.put('/scheduled-activity', json= + { + 'activity_type': 'invalidActivity', + 'message': 'test', + 'url': 'test.com', + 'start_time': '2025-01-01 00:00:00', + 'end_time': '2026-01-01 00:00:00' + }) + assert resp.status == BAD_REQUEST + assert (await resp.json())['message'] == 'Error scheduling activity: Invalid activity type' + + +async def test_put_schedule_activity_bad_start_time(api_client): + resp = await api_client.put('/scheduled-activity', json= + { + 'activity_type': 'playing', + 'message': 'test', + 'url': 'test.com', + 'start_time': 'invalid_time', + 'end_time': '2026-01-01 00:00:00' + }) + assert resp.status == BAD_REQUEST + assert (await resp.json())['message'] == 'Error scheduling activity: Bad start / end time' + + +async def test_put_schedule_activity_bad_end_time(api_client): + resp = await api_client.put('/scheduled-activity', json= + { + 'activity_type': 'invalidActivity', + 'message': 'test', + 'url': 'test.com', + 'start_time': '2026-01-01 00:00:00', + 'end_time': 'invalidTime' + }) + assert resp.status == BAD_REQUEST + assert (await resp.json())['message'] == 'Error scheduling activity: Bad start / end time' + + +''' + +PUT /activity + +''' + + +async def test_put_set_activity(api_client): + resp = await api_client.put('/activity', json= + { + 'activity_type': 'playing', + 'name': 'test', + 'url': 'test.com' + }) + assert resp.status == CREATED + text = await resp.text() + assert text == '{"message": "Activity set"}' + assert dpytest.verify().activity().matches( + discord.Activity(type=discord.ActivityType.playing, name="test", url="test.com")) + + +async def test_put_set_activity_bad_req(api_client): + resp = await api_client.put('/activity', json= + { + 'activity_type': 'invalidActivity', + 'name': 'test', + 'url': 'test.com' + }) + assert resp.status == BAD_REQUEST + assert (await resp.json())['message'] == 'Error setting activity: Invalid activity type' + + +async def test_put_set_activity_missing_param(api_client): + resp = await api_client.put('/activity', json= + { + 'activity_type': 'invalidActivity', + 'url': 'test.com' + }) + assert resp.status == BAD_REQUEST + assert (await resp.json())['message'] == "Unsatisfied Arguments: {'name'}" + + +''' + +GET /ping + +''' + + +async def test_get_ping(api_client): + with mock.patch('discord.client.Client.latency', new_callable=mock.PropertyMock) as mock_last_transaction: + mock_last_transaction.return_value = 0.42 + resp = await api_client.get('/ping') + assert resp.status == OK + text = (await resp.json())['message'] + assert "Pong! 420ms" in text + + +''' + +GET /version + +''' + + +async def test_get_version(api_client): + resp = await api_client.get('/version') + text = (await resp.json())['message'] + assert f"version: {koalabot.__version__}" in text + + +''' + +GET /support + +''' + + +async def test_get_support_link(api_client): + resp = await api_client.get('/support') + text = (await resp.json())['message'] + assert "Join our support server for more help! https://discord.gg/5etEjVd" in text + + +''' + +POST /load-cog + +''' + + +async def test_post_load_cog(api_client): + resp = await api_client.post('/load-cog', json= + { + 'extension': 'announce', + 'package': koalabot.COGS_PACKAGE + }) + assert resp.status == OK + text = (await resp.json())['message'] + assert text == "Cog loaded" + + +async def test_post_load_base_cog(api_client): + resp = await api_client.post('/load-cog', json= + { + 'extension': 'base', + 'package': koalabot.COGS_PACKAGE + }) + assert resp.status == OK + text = (await resp.json())['message'] + assert text == "Cog loaded" + + +async def test_post_load_cog_bad_req(api_client): + resp = await api_client.post('/load-cog', json= + { + 'extension': 'invalidCog', + 'package': koalabot.COGS_PACKAGE + }) + assert resp.status == BAD_REQUEST + assert (await resp.json())['message'] == 'Error loading cog: Invalid extension' + + +async def test_post_load_cog_missing_param(api_client): + resp = await api_client.post('/load-cog', json={}) + assert resp.status == BAD_REQUEST + assert (await resp.json())['message'] == "Unsatisfied Arguments: {'extension'}" + + +async def test_post_load_cog_already_loaded(api_client): + await api_client.post('/load-cog', json= + { + 'extension': 'announce', + 'package': koalabot.COGS_PACKAGE + }) + + resp = await api_client.post('/load-cog', json= + { + 'extension': 'announce', + 'package': koalabot.COGS_PACKAGE + }) + assert resp.status == BAD_REQUEST + assert (await resp.json())['message'] == 'Error loading cog: Already loaded' + + +''' + +POST /unload-cog + +''' + + +async def test_post_unload_cog(api_client): + await api_client.post('/load-cog', json= + { + 'extension': 'announce', + 'package': koalabot.COGS_PACKAGE + }) + + resp = await api_client.post('/unload-cog', json= + { + 'extension': 'announce', + 'package': koalabot.COGS_PACKAGE + }) + assert resp.status == OK + text = (await resp.json())['message'] + assert text == "Cog unloaded" + + +async def test_post_unload_cog_not_loaded(api_client): + resp = await api_client.post('/unload-cog', json= + { + 'extension': 'announce', + 'package': koalabot.COGS_PACKAGE + }) + assert resp.status == BAD_REQUEST + assert (await resp.json())['message'] == 'Error unloading cog: Extension not loaded' + + +async def test_post_unload_cog_missing_param(api_client): + resp = await api_client.post('/unload-cog', json={}) + assert resp.status == BAD_REQUEST + assert (await resp.json())['message'] == "Unsatisfied Arguments: {'extension'}" + + +async def test_post_unload_base_cog(api_client): + resp = await api_client.post('/unload-cog', json= + { + 'extension': 'BaseCog', + 'package': koalabot.COGS_PACKAGE + }) + assert resp.status == BAD_REQUEST + text = (await resp.json())['message'] + assert text == "Error unloading cog: Sorry, you can't unload the base cog" + + +''' + +POST /enable-extension + +''' + + +@mock.patch("koalabot.ENABLED_COGS", ["announce"]) +async def test_post_enable_extension(api_client, bot): + await koalabot.load_all_cogs(bot) + guild: discord.Guild = dpytest.get_config().guilds[0] + resp = await api_client.post('/enable-extension', json={ + 'guild_id': guild.id, + 'koala_ext': 'Announce' + }) + + assert resp.status == OK + text = (await resp.json())['message'] + assert text == "Extension enabled" + + +async def test_post_enable_extension_bad_req(api_client): + guild: discord.Guild = dpytest.get_config().guilds[0] + + resp = await api_client.post('/enable-extension', json= + { + 'guild_id': guild.id, + 'koala_ext': 'Invalid Extension' + }) + assert resp.status == BAD_REQUEST + text = (await resp.json())['message'] + assert text == "Error enabling extension: Invalid extension" + + +async def test_post_enable_extension_missing_param(api_client): + guild: discord.Guild = dpytest.get_config().guilds[0] + resp = await api_client.post('/enable-extension', json= + { + 'guild_id': guild.id + }) + assert resp.status == BAD_REQUEST + text = (await resp.json())['message'] + assert text == "Unsatisfied Arguments: {'koala_ext'}" + + +''' + +POST /disable-extension + +''' + + +@mock.patch("koalabot.ENABLED_COGS", ['announce']) +async def test_post_disable_extension(api_client, bot): + await koalabot.load_all_cogs(bot) + guild: discord.Guild = dpytest.get_config().guilds[0] + setup = await api_client.post('/enable-extension', json={ + 'guild_id': guild.id, + 'koala_ext': 'Announce' + }) + assert setup.status == OK + + resp = await api_client.post('/disable-extension', json={ + 'guild_id': guild.id, + 'koala_ext': 'Announce' + }) + assert resp.status == OK + text = await resp.text() + assert text == '{"message": "Extension disabled"}' + + +async def test_post_disable_extension_not_enabled(api_client): + guild: discord.Guild = dpytest.get_config().guilds[0] + resp = await api_client.post('/disable-extension', json={ + 'guild_id': guild.id, + 'koala_ext': 'Announce' + }) + assert resp.status == BAD_REQUEST + text = (await resp.json())['message'] + assert text == "Error disabling extension: Extension not enabled" + + +async def test_post_disable_extension_missing_param(api_client): + guild: discord.Guild = dpytest.get_config().guilds[0] + resp = await api_client.post('/disable-extension', json={ + 'guild_id': guild.id + }) + assert resp.status == BAD_REQUEST + text = (await resp.json())['message'] + assert text == "Unsatisfied Arguments: {'koala_ext'}" + + +async def test_post_disable_extension_bad_req(api_client): + guild: discord.Guild = dpytest.get_config().guilds[0] + + resp = await api_client.post('/disable-extension', json= + { + 'guild_id': guild.id, + 'koala_ext': 'Invalid Extension' + }) + assert resp.status == BAD_REQUEST + text = (await resp.json())['message'] + assert text == "Error disabling extension: Extension not enabled" + + +''' + +GET /extensions + +''' + + +@mock.patch("koalabot.ENABLED_COGS", ['announce']) +async def test_get_extension(api_client, bot): + await koalabot.load_all_cogs(bot) + guild: discord.Guild = dpytest.get_config().guilds[0] + resp = await api_client.get('/extensions?guild_id={}'.format(guild.id)) + assert resp.status == OK + text = await resp.text() + assert text == '["Announce"]' + + +async def test_get_extension_bad_param(api_client): + resp = await api_client.get('/extensions?invalid-arg=abc') + assert resp.status == BAD_REQUEST + text = (await resp.json())['message'] + assert text == "Unsatisfied Arguments: {'guild_id'}" + + +async def test_get_extension_missing_param(api_client): + resp = await api_client.get('/extensions') + assert resp.status == BAD_REQUEST + text = (await resp.json())['message'] + assert text == "Unsatisfied Arguments: {'guild_id'}" diff --git a/tests/cogs/base/test_cog.py b/tests/koala/cogs/base/test_cog.py similarity index 91% rename from tests/cogs/base/test_cog.py rename to tests/koala/cogs/base/test_cog.py index f9439d39..7b7c1ea8 100644 --- a/tests/cogs/base/test_cog.py +++ b/tests/koala/cogs/base/test_cog.py @@ -45,7 +45,7 @@ async def base_cog(bot: commands.Bot): return cog -@mock.patch("koalabot.COGS_PACKAGE", "tests.tests_utils.fake_load_all_cogs") +@mock.patch("koalabot.COGS_PACKAGE", "tests.koala.tests_utils.fake_load_all_cogs") @mock.patch("koalabot.ENABLED_COGS", ["greetings_cog"]) @pytest.mark.asyncio async def test_list_koala_ext_disabled(bot, base_cog): @@ -59,7 +59,7 @@ async def test_list_koala_ext_disabled(bot, base_cog): assert dpytest.verify().message().embed(embed=expected_embed) -@mock.patch("koalabot.COGS_PACKAGE", "tests.tests_utils.fake_load_all_cogs") +@mock.patch("koalabot.COGS_PACKAGE", "tests.koala.tests_utils.fake_load_all_cogs") @mock.patch("koalabot.ENABLED_COGS", ['greetings_cog']) @pytest.mark.asyncio async def test_enable_koala_ext(bot, base_cog): @@ -73,7 +73,7 @@ async def test_enable_koala_ext(bot, base_cog): assert dpytest.verify().message().embed(embed=expected_embed) -@mock.patch("koalabot.COGS_PACKAGE", "tests.tests_utils.fake_load_all_cogs") +@mock.patch("koalabot.COGS_PACKAGE", "tests.koala.tests_utils.fake_load_all_cogs") @mock.patch("koalabot.ENABLED_COGS", ['greetings_cog']) @pytest.mark.asyncio async def test_disable_koala_ext(bot, base_cog): @@ -152,12 +152,6 @@ async def test_ping(base_cog: BaseCog): assert dpytest.verify().message().content("Pong! 4ms") -@pytest.mark.asyncio -async def test_support(): - await dpytest.message(koalabot.COMMAND_PREFIX + "support") - assert dpytest.verify().message().content("Join our support server for more help! https://discord.gg/5etEjVd") - - @pytest.mark.asyncio async def test_default_clear(): with mock.patch.object(discord.TextChannel, 'purge') as mock1: @@ -199,24 +193,24 @@ async def test_unload_base_cog(base_cog: BaseCog): await dpytest.message(koalabot.COMMAND_PREFIX + "unload_cog BaseCog") -@mock.patch("koalabot.COGS_PACKAGE", "tests.tests_utils.fake_load_all_cogs") +@mock.patch("koalabot.COGS_PACKAGE", "tests.koala.tests_utils.fake_load_all_cogs") @pytest.mark.asyncio async def test_load_valid_cog(base_cog: BaseCog): with mock.patch.object(discord.ext.commands.bot.Bot, 'load_extension') as mock1: await dpytest.message(koalabot.COMMAND_PREFIX + "load_cog Greetings") - mock1.assert_called_with(".Greetings", package="tests.tests_utils.fake_load_all_cogs") + mock1.assert_called_with(".Greetings", package="tests.koala.tests_utils.fake_load_all_cogs") -@mock.patch("koalabot.COGS_PACKAGE", "tests.tests_utils.fake_load_all_cogs") +@mock.patch("koalabot.COGS_PACKAGE", "tests.koala.tests_utils.fake_load_all_cogs") @pytest.mark.asyncio async def test_load_and_unload_valid_cog(base_cog: BaseCog): with mock.patch.object(discord.ext.commands.bot.Bot, 'load_extension') as mock1: await dpytest.message(koalabot.COMMAND_PREFIX + "load_cog Greetings") - mock1.assert_called_with(".Greetings", package="tests.tests_utils.fake_load_all_cogs") + mock1.assert_called_with(".Greetings", package="tests.koala.tests_utils.fake_load_all_cogs") with mock.patch.object(discord.ext.commands.bot.Bot, 'unload_extension') as mock1: await dpytest.message(koalabot.COMMAND_PREFIX + "unload_cog Greetings") - mock1.assert_called_with(".Greetings", package="tests.tests_utils.fake_load_all_cogs") + mock1.assert_called_with(".Greetings", package="tests.koala.tests_utils.fake_load_all_cogs") @pytest.mark.asyncio diff --git a/tests/cogs/base/test_core.py b/tests/koala/cogs/base/test_core.py similarity index 100% rename from tests/cogs/base/test_core.py rename to tests/koala/cogs/base/test_core.py diff --git a/tests/cogs/base/test_db.py b/tests/koala/cogs/base/test_db.py similarity index 100% rename from tests/cogs/base/test_db.py rename to tests/koala/cogs/base/test_db.py diff --git a/tests/cogs/insights/__init__.py b/tests/koala/cogs/colour_role/__init__.py similarity index 100% rename from tests/cogs/insights/__init__.py rename to tests/koala/cogs/colour_role/__init__.py diff --git a/tests/cogs/colour_role/test_cog.py b/tests/koala/cogs/colour_role/test_cog.py similarity index 99% rename from tests/cogs/colour_role/test_cog.py rename to tests/koala/cogs/colour_role/test_cog.py index 64f8c9b2..4ee12722 100644 --- a/tests/cogs/colour_role/test_cog.py +++ b/tests/koala/cogs/colour_role/test_cog.py @@ -25,7 +25,7 @@ from koala.cogs import colour_role from koala.cogs.colour_role.utils import COLOUR_ROLE_NAMING from tests.log import logger -from tests.tests_utils.last_ctx_cog import LastCtxCog +from tests.koala.tests_utils.last_ctx_cog import LastCtxCog from .utils import make_list_of_roles, make_list_of_custom_colour_roles, make_list_of_protected_colour_roles, \ random_colour, independent_get_protected_colours, independent_get_colour_change_roles, DBManager diff --git a/tests/cogs/colour_role/test_db.py b/tests/koala/cogs/colour_role/test_db.py similarity index 100% rename from tests/cogs/colour_role/test_db.py rename to tests/koala/cogs/colour_role/test_db.py diff --git a/tests/cogs/colour_role/utils.py b/tests/koala/cogs/colour_role/utils.py similarity index 100% rename from tests/cogs/colour_role/utils.py rename to tests/koala/cogs/colour_role/utils.py diff --git a/tests/cogs/intro_cog/__init__.py b/tests/koala/cogs/insights/__init__.py similarity index 100% rename from tests/cogs/intro_cog/__init__.py rename to tests/koala/cogs/insights/__init__.py diff --git a/tests/cogs/insights/test_cog.py b/tests/koala/cogs/insights/test_cog.py similarity index 100% rename from tests/cogs/insights/test_cog.py rename to tests/koala/cogs/insights/test_cog.py diff --git a/tests/cogs/react_for_role/__init__.py b/tests/koala/cogs/intro_cog/__init__.py similarity index 100% rename from tests/cogs/react_for_role/__init__.py rename to tests/koala/cogs/intro_cog/__init__.py diff --git a/tests/cogs/intro_cog/conftest.py b/tests/koala/cogs/intro_cog/conftest.py similarity index 92% rename from tests/cogs/intro_cog/conftest.py rename to tests/koala/cogs/intro_cog/conftest.py index 48b35b93..0d7e1d40 100644 --- a/tests/cogs/intro_cog/conftest.py +++ b/tests/koala/cogs/intro_cog/conftest.py @@ -16,7 +16,7 @@ from koala.cogs import IntroCog from tests.log import logger -from tests.tests_utils.last_ctx_cog import LastCtxCog +from tests.koala.tests_utils.last_ctx_cog import LastCtxCog @pytest_asyncio.fixture(autouse=True) diff --git a/tests/cogs/intro_cog/test_cog.py b/tests/koala/cogs/intro_cog/test_cog.py similarity index 100% rename from tests/cogs/intro_cog/test_cog.py rename to tests/koala/cogs/intro_cog/test_cog.py diff --git a/tests/cogs/intro_cog/test_db.py b/tests/koala/cogs/intro_cog/test_db.py similarity index 100% rename from tests/cogs/intro_cog/test_db.py rename to tests/koala/cogs/intro_cog/test_db.py diff --git a/tests/cogs/intro_cog/test_utils.py b/tests/koala/cogs/intro_cog/test_utils.py similarity index 100% rename from tests/cogs/intro_cog/test_utils.py rename to tests/koala/cogs/intro_cog/test_utils.py diff --git a/tests/cogs/intro_cog/utils.py b/tests/koala/cogs/intro_cog/utils.py similarity index 100% rename from tests/cogs/intro_cog/utils.py rename to tests/koala/cogs/intro_cog/utils.py diff --git a/tests/cogs/text_filter/__init__.py b/tests/koala/cogs/react_for_role/__init__.py similarity index 100% rename from tests/cogs/text_filter/__init__.py rename to tests/koala/cogs/react_for_role/__init__.py diff --git a/tests/cogs/react_for_role/conftest.py b/tests/koala/cogs/react_for_role/conftest.py similarity index 95% rename from tests/cogs/react_for_role/conftest.py rename to tests/koala/cogs/react_for_role/conftest.py index 27239ae6..55e97bd4 100644 --- a/tests/cogs/react_for_role/conftest.py +++ b/tests/koala/cogs/react_for_role/conftest.py @@ -20,7 +20,7 @@ from koala.cogs.react_for_role.models import GuildRFRRequiredRoles, GuildRFRMessages, RFRMessageEmojiRoles from koala.db import session_manager from tests.log import logger -from tests.tests_utils.last_ctx_cog import LastCtxCog +from tests.koala.tests_utils.last_ctx_cog import LastCtxCog # Constants diff --git a/tests/cogs/react_for_role/test_api.py b/tests/koala/cogs/react_for_role/test_api.py similarity index 100% rename from tests/cogs/react_for_role/test_api.py rename to tests/koala/cogs/react_for_role/test_api.py diff --git a/tests/cogs/react_for_role/test_cog.py b/tests/koala/cogs/react_for_role/test_cog.py similarity index 99% rename from tests/cogs/react_for_role/test_cog.py rename to tests/koala/cogs/react_for_role/test_cog.py index 190caf3d..98634ed8 100644 --- a/tests/cogs/react_for_role/test_cog.py +++ b/tests/koala/cogs/react_for_role/test_cog.py @@ -25,7 +25,7 @@ from koala.cogs.react_for_role.db import * from koala.colours import KOALA_GREEN from tests.log import logger -from tests.tests_utils import utils as testutils +from tests.koala.tests_utils import utils as testutils from .utils import independent_get_guild_rfr_message, independent_get_guild_rfr_required_role @@ -463,10 +463,8 @@ async def test_rfr_edit_thumbnail_bad_attach(attach): @pytest.mark.asyncio @pytest.mark.parametrize("image_url", [ - "https://media.discordapp.net/attachments/611574654502699010/756152703801098280/IMG_20200917_150032.jpg", "https://images-ext-1.discordapp.net/external/to2H6kvblcjDUm5Smwx4rSqwCPTP-UDFdWp1ToEXJQM/https/cdn.weeb.sh/images/Hk9GpT_Pb.png?width=864&height=660", - "https://cdn.weeb.sh/images/Hk9GpT_Pb.png", - "https://cdn.discordapp.com/attachments/611574654502699010/828026462552457266/unknown.png"]) + "https://cdn.weeb.sh/images/Hk9GpT_Pb.png"]) async def test_rfr_edit_thumbnail_links(image_url): config: dpytest.RunnerConfig = dpytest.get_config() guild: discord.Guild = config.guilds[0] diff --git a/tests/cogs/react_for_role/test_db.py b/tests/koala/cogs/react_for_role/test_db.py similarity index 99% rename from tests/cogs/react_for_role/test_db.py rename to tests/koala/cogs/react_for_role/test_db.py index 8ccc911b..d6edfc4c 100644 --- a/tests/cogs/react_for_role/test_db.py +++ b/tests/koala/cogs/react_for_role/test_db.py @@ -21,7 +21,7 @@ from koala.cogs.react_for_role.db import * from tests.log import logger # Own modules -from tests.tests_utils import utils as testutils +from tests.koala.tests_utils import utils as testutils from .utils import independent_get_guild_rfr_message, independent_get_rfr_message_emoji_role, \ independent_get_guild_rfr_required_role, get_rfr_reaction_role_by_role_id diff --git a/tests/cogs/react_for_role/utils.py b/tests/koala/cogs/react_for_role/utils.py similarity index 100% rename from tests/cogs/react_for_role/utils.py rename to tests/koala/cogs/react_for_role/utils.py diff --git a/tests/cogs/twitch_alert/__init__.py b/tests/koala/cogs/text_filter/__init__.py similarity index 100% rename from tests/cogs/twitch_alert/__init__.py rename to tests/koala/cogs/text_filter/__init__.py diff --git a/tests/cogs/text_filter/test_cog.py b/tests/koala/cogs/text_filter/test_cog.py similarity index 99% rename from tests/cogs/text_filter/test_cog.py rename to tests/koala/cogs/text_filter/test_cog.py index d4941e82..03e3f699 100644 --- a/tests/cogs/text_filter/test_cog.py +++ b/tests/koala/cogs/text_filter/test_cog.py @@ -20,7 +20,7 @@ from koala.db import session_manager from koala.utils import is_int from tests.log import logger -from tests.tests_utils.last_ctx_cog import LastCtxCog +from tests.koala.tests_utils.last_ctx_cog import LastCtxCog # Variables diff --git a/tests/cogs/verification/__init__.py b/tests/koala/cogs/twitch_alert/__init__.py similarity index 100% rename from tests/cogs/verification/__init__.py rename to tests/koala/cogs/twitch_alert/__init__.py diff --git a/tests/cogs/twitch_alert/test_cog.py b/tests/koala/cogs/twitch_alert/test_cog.py similarity index 97% rename from tests/cogs/twitch_alert/test_cog.py rename to tests/koala/cogs/twitch_alert/test_cog.py index ebd1571a..a675bc51 100644 --- a/tests/cogs/twitch_alert/test_cog.py +++ b/tests/koala/cogs/twitch_alert/test_cog.py @@ -19,7 +19,9 @@ from koala.cogs.twitch_alert.models import UserInTwitchAlert from koala.colours import KOALA_GREEN from koala.db import session_manager -from tests.tests_utils.last_ctx_cog import LastCtxCog +from koala.kb2.models import Guild, ExtensionAttr +from koala.models import Guilds +from tests.koala.tests_utils.last_ctx_cog import LastCtxCog # Constants DB_PATH = "Koala.db" @@ -44,18 +46,20 @@ async def twitch_cog(bot: discord.ext.commands.Bot): return twitch_cog -@mock.patch("koalabot.check_guild_has_ext", mock.MagicMock(return_value=True)) +@mock.patch("koalabot.check_guild_has_ext", mock.MagicMock(return_value=None)) def test_twitch_is_enabled_true(twitch_cog): - assert cog.twitch_is_enabled(None) + assert cog.is_enabled(None) @mock.patch("koalabot.is_dm_channel", mock.MagicMock(return_value=True)) +@mock.patch("koalabot.is_dpytest", False) def test_twitch_is_enabled_dm(): - assert not cog.twitch_is_enabled(None) + assert not cog.is_enabled(None) @mock.patch("koalabot.is_dm_channel", mock.MagicMock(return_value=False)) @mock.patch("koalabot.is_dpytest", False) +@mock.patch("koala.kb2.models.Guild.get", mock.MagicMock(return_value=Guild(extensions=[ExtensionAttr(id="TwitchAlert",version=1,enabled=True)]))) @pytest.mark.asyncio async def test_twitch_is_enabled_false(twitch_cog: cog.TwitchAlert): last_ctx_cog = LastCtxCog(bot=twitch_cog.bot) @@ -63,7 +67,7 @@ async def test_twitch_is_enabled_false(twitch_cog: cog.TwitchAlert): await dpytest.message(koalabot.COMMAND_PREFIX + "store_ctx", channel=-1) ctx: commands.Context = last_ctx_cog.get_last_ctx() - assert not cog.twitch_is_enabled(ctx) + assert not cog.is_enabled(ctx) # @mock.patch("koala.utils.random_id", mock.MagicMock(return_value=7357)) diff --git a/tests/cogs/twitch_alert/test_db.py b/tests/koala/cogs/twitch_alert/test_db.py similarity index 100% rename from tests/cogs/twitch_alert/test_db.py rename to tests/koala/cogs/twitch_alert/test_db.py diff --git a/tests/cogs/twitch_alert/test_twitch_handler.py b/tests/koala/cogs/twitch_alert/test_twitch_handler.py similarity index 100% rename from tests/cogs/twitch_alert/test_twitch_handler.py rename to tests/koala/cogs/twitch_alert/test_twitch_handler.py diff --git a/tests/cogs/twitch_alert/test_utils.py b/tests/koala/cogs/twitch_alert/test_utils.py similarity index 100% rename from tests/cogs/twitch_alert/test_utils.py rename to tests/koala/cogs/twitch_alert/test_utils.py diff --git a/tests/cogs/voting/__init__.py b/tests/koala/cogs/verification/__init__.py similarity index 100% rename from tests/cogs/voting/__init__.py rename to tests/koala/cogs/verification/__init__.py diff --git a/tests/cogs/verification/test_api.py b/tests/koala/cogs/verification/test_api.py similarity index 100% rename from tests/cogs/verification/test_api.py rename to tests/koala/cogs/verification/test_api.py diff --git a/tests/cogs/verification/test_cog.py b/tests/koala/cogs/verification/test_cog.py similarity index 100% rename from tests/cogs/verification/test_cog.py rename to tests/koala/cogs/verification/test_cog.py diff --git a/tests/cogs/verification/test_core.py b/tests/koala/cogs/verification/test_core.py similarity index 100% rename from tests/cogs/verification/test_core.py rename to tests/koala/cogs/verification/test_core.py diff --git a/tests/koala/cogs/voting/__init__.py b/tests/koala/cogs/voting/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/cogs/voting/conftest.py b/tests/koala/cogs/voting/conftest.py similarity index 100% rename from tests/cogs/voting/conftest.py rename to tests/koala/cogs/voting/conftest.py diff --git a/tests/cogs/voting/test_cog.py b/tests/koala/cogs/voting/test_cog.py similarity index 100% rename from tests/cogs/voting/test_cog.py rename to tests/koala/cogs/voting/test_cog.py diff --git a/tests/cogs/voting/test_db.py b/tests/koala/cogs/voting/test_db.py similarity index 100% rename from tests/cogs/voting/test_db.py rename to tests/koala/cogs/voting/test_db.py diff --git a/tests/cogs/voting/test_option.py b/tests/koala/cogs/voting/test_option.py similarity index 100% rename from tests/cogs/voting/test_option.py rename to tests/koala/cogs/voting/test_option.py diff --git a/tests/cogs/voting/test_two_way.py b/tests/koala/cogs/voting/test_two_way.py similarity index 100% rename from tests/cogs/voting/test_two_way.py rename to tests/koala/cogs/voting/test_two_way.py diff --git a/tests/cogs/voting/test_vote.py b/tests/koala/cogs/voting/test_vote.py similarity index 100% rename from tests/cogs/voting/test_vote.py rename to tests/koala/cogs/voting/test_vote.py diff --git a/tests/cogs/voting/utils.py b/tests/koala/cogs/voting/utils.py similarity index 100% rename from tests/cogs/voting/utils.py rename to tests/koala/cogs/voting/utils.py diff --git a/tests/koala/conftest.py b/tests/koala/conftest.py new file mode 100644 index 00000000..cc87814b --- /dev/null +++ b/tests/koala/conftest.py @@ -0,0 +1,41 @@ +""" +A configuration file for methods useful in all testing with pytest +""" +# Futures + +# Built-in/Generic Imports + +import discord +import discord.ext.test as dpytest +# Libs +import pytest +import pytest_asyncio + +import koala.db as db +# Own modules +import koalabot + +# Constants + + +@pytest_asyncio.fixture +async def bot(): + import koalabot + intents = discord.Intents.default() + intents.members = True + intents.guilds = True + intents.messages = True + intents.message_content = True + b = koalabot.KoalaBot(koalabot.COMMAND_PREFIX, intents=intents) + await b._async_setup_hook() + await dpytest.empty_queue() + dpytest.configure(b) + return b + + +@pytest.fixture(autouse=True) +def setup_is_dpytest(): + db.__create_sqlite_tables() + koalabot.is_dpytest = True + yield + koalabot.is_dpytest = False diff --git a/tests/test_koalabot.py b/tests/koala/test_koalabot.py similarity index 89% rename from tests/test_koalabot.py rename to tests/koala/test_koalabot.py index a4f66675..40ae105e 100644 --- a/tests/test_koalabot.py +++ b/tests/koala/test_koalabot.py @@ -1,141 +1,141 @@ -#!/usr/bin/env python - -""" -Testing KoalaBot Base Code - -Commented using reStructuredText (reST) -""" -# Futures - -# Built-in/Generic Imports - -# Libs -import discord -import discord.ext.test as dpytest -import mock -import pytest -import pytest_asyncio -from discord.ext import commands - -# Own modules -import koalabot -from koala.db import clear_all_tables, fetch_all_tables -from tests.tests_utils.last_ctx_cog import LastCtxCog -from tests.tests_utils.utils import FakeAuthor - -# Constants - -# Variables -utils_cog = None - - -@pytest_asyncio.fixture(autouse=True) -async def test_ctx(bot: commands.Bot): - global utils_cog - utils_cog = LastCtxCog(bot) - await bot.add_cog(utils_cog) - dpytest.configure(bot) - await dpytest.message(koalabot.COMMAND_PREFIX + "store_ctx") - return utils_cog.get_last_ctx() - - -@pytest.fixture(scope='session', autouse=True) -def setup_db(): - clear_all_tables(fetch_all_tables()) - - -@pytest_asyncio.fixture(scope='function', autouse=True) -async def setup_clean_messages(): - await dpytest.empty_queue() - yield dpytest - - -def test_test_user_is_owner(test_ctx): - assert koalabot.is_owner(test_ctx) - - -def test_invalid_test_user_is_owner(test_ctx): - for i in range(len(koalabot.BOT_OWNER)): - test_ctx.author = FakeAuthor(id=koalabot.BOT_OWNER[i] + 1) - koalabot.is_dpytest = False - assert not koalabot.is_owner(test_ctx) - koalabot.is_dpytest = True - - -def test_owner_is_owner(test_ctx): - for i in range(len(koalabot.BOT_OWNER)): - test_ctx.author = FakeAuthor(id=(koalabot.BOT_OWNER[i])) - assert koalabot.is_owner(test_ctx) - - -def test_test_user_is_admin(test_ctx): - assert koalabot.is_admin(test_ctx) - - -def test_invalid_test_user_is_admin(test_ctx): - test_ctx.author = FakeAuthor(id=int(koalabot.BOT_OWNER[0]) + 2) - koalabot.is_dpytest = False - assert not koalabot.is_admin(test_ctx) - koalabot.is_dpytest = True - - -def test_admin_test_user_is_admin(test_ctx): - test_ctx.author = FakeAuthor(name="TestUser#0001", all_permissions=True) - assert koalabot.is_admin(test_ctx) - - -def test_admin_is_admin(test_ctx): - test_ctx.author = FakeAuthor(name="TestUser#0002", all_permissions=True) - assert koalabot.is_admin(test_ctx) - - -def test_not_admin_is_admin(test_ctx): - test_ctx.author = FakeAuthor(all_permissions=False) - koalabot.is_dpytest = False - assert not koalabot.is_admin(test_ctx) - koalabot.is_dpytest = True - - -@mock.patch("koalabot.COGS_PACKAGE", "tests.tests_utils.fake_load_all_cogs") -@mock.patch("koalabot.ENABLED_COGS", ['greetings_cog']) -@pytest.mark.asyncio -async def test_load_all_cogs(bot): - with mock.patch.object(discord.ext.commands.bot.Bot, 'load_extension') as mock1: - await koalabot.load_all_cogs(bot) - mock1.assert_called_with(".greetings_cog", package="tests.tests_utils.fake_load_all_cogs") - - -@pytest.mark.asyncio -async def test_dm_single_group_message(): - test_message = 'default message' - test_member = dpytest.get_config().members[0] - x = await koalabot.dm_group_message([test_member], test_message) - assert dpytest.verify().message().content(test_message) - assert x == 1 - - -@pytest.mark.asyncio -async def test_dm_plural_group_message(): - test_message = 'default message' - test_member = dpytest.get_config().members[0] - test_member_2 = await dpytest.member_join() - await dpytest.empty_queue() - x = await koalabot.dm_group_message([test_member, test_member_2], test_message) - assert dpytest.verify().message().content(test_message) - assert dpytest.verify().message().content(test_message) - assert x == 2 - - -@pytest.mark.asyncio -async def test_dm_empty_group_message(): - test_message = 'this should not be sent' - x = await koalabot.dm_group_message([], test_message) - assert dpytest.verify().message().nothing() - assert x == 0 - - -@pytest.fixture(scope='session', autouse=True) -def setup_is_dpytest(): - koalabot.is_dpytest = True - yield - koalabot.is_dpytest = False +#!/usr/bin/env python + +""" +Testing KoalaBot Base Code + +Commented using reStructuredText (reST) +""" +# Futures + +# Built-in/Generic Imports + +# Libs +import discord +import discord.ext.test as dpytest +import mock +import pytest +import pytest_asyncio +from discord.ext import commands + +# Own modules +import koalabot +from koala.db import clear_all_tables, fetch_all_tables +from tests.koala.tests_utils.last_ctx_cog import LastCtxCog +from tests.koala.tests_utils.utils import FakeAuthor + +# Constants + +# Variables +utils_cog = None + + +@pytest_asyncio.fixture(autouse=True) +async def test_ctx(bot: commands.Bot): + global utils_cog + utils_cog = LastCtxCog(bot) + await bot.add_cog(utils_cog) + dpytest.configure(bot) + await dpytest.message(koalabot.COMMAND_PREFIX + "store_ctx") + return utils_cog.get_last_ctx() + + +@pytest.fixture(scope='session', autouse=True) +def setup_db(): + clear_all_tables(fetch_all_tables()) + + +@pytest_asyncio.fixture(scope='function', autouse=True) +async def setup_clean_messages(): + await dpytest.empty_queue() + yield dpytest + + +def test_test_user_is_owner(test_ctx): + assert koalabot.is_owner(test_ctx) + + +def test_invalid_test_user_is_owner(test_ctx): + for i in range(len(koalabot.BOT_OWNER)): + test_ctx.author = FakeAuthor(id=koalabot.BOT_OWNER[i] + 1) + koalabot.is_dpytest = False + assert not koalabot.is_owner(test_ctx) + koalabot.is_dpytest = True + + +def test_owner_is_owner(test_ctx): + for i in range(len(koalabot.BOT_OWNER)): + test_ctx.author = FakeAuthor(id=(koalabot.BOT_OWNER[i])) + assert koalabot.is_owner(test_ctx) + + +def test_test_user_is_admin(test_ctx): + assert koalabot.is_admin(test_ctx) + + +def test_invalid_test_user_is_admin(test_ctx): + test_ctx.author = FakeAuthor(id=int(koalabot.BOT_OWNER[0]) + 2) + koalabot.is_dpytest = False + assert not koalabot.is_admin(test_ctx) + koalabot.is_dpytest = True + + +def test_admin_test_user_is_admin(test_ctx): + test_ctx.author = FakeAuthor(name="TestUser#0001", all_permissions=True) + assert koalabot.is_admin(test_ctx) + + +def test_admin_is_admin(test_ctx): + test_ctx.author = FakeAuthor(name="TestUser#0002", all_permissions=True) + assert koalabot.is_admin(test_ctx) + + +def test_not_admin_is_admin(test_ctx): + test_ctx.author = FakeAuthor(all_permissions=False) + koalabot.is_dpytest = False + assert not koalabot.is_admin(test_ctx) + koalabot.is_dpytest = True + + +@mock.patch("koalabot.COGS_PACKAGE", "tests.koala.tests_utils.fake_load_all_cogs") +@mock.patch("koalabot.ENABLED_COGS", ['greetings_cog']) +@pytest.mark.asyncio +async def test_load_all_cogs(bot): + with mock.patch.object(discord.ext.commands.bot.Bot, 'load_extension') as mock1: + await koalabot.load_all_cogs(bot) + mock1.assert_called_with(".greetings_cog", package="tests.koala.tests_utils.fake_load_all_cogs") + + +@pytest.mark.asyncio +async def test_dm_single_group_message(): + test_message = 'default message' + test_member = dpytest.get_config().members[0] + x = await koalabot.dm_group_message([test_member], test_message) + assert dpytest.verify().message().content(test_message) + assert x == 1 + + +@pytest.mark.asyncio +async def test_dm_plural_group_message(): + test_message = 'default message' + test_member = dpytest.get_config().members[0] + test_member_2 = await dpytest.member_join() + await dpytest.empty_queue() + x = await koalabot.dm_group_message([test_member, test_member_2], test_message) + assert dpytest.verify().message().content(test_message) + assert dpytest.verify().message().content(test_message) + assert x == 2 + + +@pytest.mark.asyncio +async def test_dm_empty_group_message(): + test_message = 'this should not be sent' + x = await koalabot.dm_group_message([], test_message) + assert dpytest.verify().message().nothing() + assert x == 0 + + +@pytest.fixture(scope='session', autouse=True) +def setup_is_dpytest(): + koalabot.is_dpytest = True + yield + koalabot.is_dpytest = False diff --git a/tests/test_utils.py b/tests/koala/test_utils.py similarity index 97% rename from tests/test_utils.py rename to tests/koala/test_utils.py index 71c893ea..9bb2e38d 100644 --- a/tests/test_utils.py +++ b/tests/koala/test_utils.py @@ -21,7 +21,7 @@ import koalabot from koala.utils import __parse_args, format_config_path, wait_for_message from tests.log import logger -from tests.tests_utils.last_ctx_cog import LastCtxCog +from tests.koala.tests_utils.last_ctx_cog import LastCtxCog # Constants diff --git a/tests/tests_utils/__init__.py b/tests/koala/tests_utils/__init__.py similarity index 100% rename from tests/tests_utils/__init__.py rename to tests/koala/tests_utils/__init__.py diff --git a/tests/tests_utils/fake_load_all_cogs/__init__.py b/tests/koala/tests_utils/fake_load_all_cogs/__init__.py similarity index 100% rename from tests/tests_utils/fake_load_all_cogs/__init__.py rename to tests/koala/tests_utils/fake_load_all_cogs/__init__.py diff --git a/tests/tests_utils/fake_load_all_cogs/greetings_cog.py b/tests/koala/tests_utils/fake_load_all_cogs/greetings_cog.py similarity index 100% rename from tests/tests_utils/fake_load_all_cogs/greetings_cog.py rename to tests/koala/tests_utils/fake_load_all_cogs/greetings_cog.py diff --git a/tests/tests_utils/last_ctx_cog.py b/tests/koala/tests_utils/last_ctx_cog.py similarity index 100% rename from tests/tests_utils/last_ctx_cog.py rename to tests/koala/tests_utils/last_ctx_cog.py diff --git a/tests/tests_utils/utils.py b/tests/koala/tests_utils/utils.py similarity index 100% rename from tests/tests_utils/utils.py rename to tests/koala/tests_utils/utils.py diff --git a/tests/pytest.ini b/tests/pytest.ini index d1f3727e..a3bd4a66 100644 --- a/tests/pytest.ini +++ b/tests/pytest.ini @@ -8,3 +8,4 @@ env = CONFIG_PATH=./config-testing LOGGING_FILE=False DB_TYPE=SQLITE + KB2_ENABLED=False \ No newline at end of file