From ec4a461d35e5bf088a4c63b3a2ef139e15b609f9 Mon Sep 17 00:00:00 2001 From: Jack Draper Date: Sat, 11 Nov 2023 07:55:19 +0000 Subject: [PATCH 01/16] Revert "fix: remove pysqlcipher" This reverts commit 7dfadeb65dabad4e82f27512d9283c9847b73f90. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c492b6ef..0b98a4e4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,7 +31,7 @@ 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 From 4e89612cea0bb5339e4834580cbfbf9126aa9c13 Mon Sep 17 00:00:00 2001 From: Jack Draper Date: Sat, 11 Nov 2023 07:55:24 +0000 Subject: [PATCH 02/16] Revert "fix: remove sqlcipher from dockerfile" This reverts commit b3058a5225f966938adbe1559d299c45eff24e84. --- Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0b98a4e4..8b555e85 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,12 +19,12 @@ 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.3.0-0~202102181541~462~202104031456~ubuntu20.04.1 \ + libsqlcipher-dev=4.3.0-0~202102181541~462~202104031456~ubuntu20.04.1 COPY . /app WORKDIR /app From 87e6938d8742d65ddcfb68119937e7c1b4245086 Mon Sep 17 00:00:00 2001 From: Jack Draper Date: Sat, 11 Nov 2023 07:59:38 +0000 Subject: [PATCH 03/16] chore: upgrade sqlcipher versions --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8b555e85..9309ddb2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,8 +23,8 @@ RUN apt-get install -y software-properties-common && \ 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 + 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 From e7f1c00534c9f53be57bcbd8a6d3152e0494d1c4 Mon Sep 17 00:00:00 2001 From: Jack Draper Date: Sat, 11 Nov 2023 08:06:30 +0000 Subject: [PATCH 04/16] fix: remove verify blacklist from migration --- alembic/versions/dd3c60f39768_mysql_migration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/alembic/versions/dd3c60f39768_mysql_migration.py b/alembic/versions/dd3c60f39768_mysql_migration.py index bc284036..d40b02fb 100644 --- a/alembic/versions/dd3c60f39768_mysql_migration.py +++ b/alembic/versions/dd3c60f39768_mysql_migration.py @@ -364,8 +364,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") From 49a273c2d0a64f0f586d3cc220ceaa9b9ef98769 Mon Sep 17 00:00:00 2001 From: Jack Draper Date: Sat, 11 Nov 2023 08:33:02 +0000 Subject: [PATCH 05/16] fix: remove foreign key restraint in migration prod has bad data --- alembic/versions/dd3c60f39768_mysql_migration.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/alembic/versions/dd3c60f39768_mysql_migration.py b/alembic/versions/dd3c60f39768_mysql_migration.py index d40b02fb..db09b3f3 100644 --- a/alembic/versions/dd3c60f39768_mysql_migration.py +++ b/alembic/versions/dd3c60f39768_mysql_migration.py @@ -220,7 +220,8 @@ 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)) From 6860093ca30d4f6764ac50e238b75fe3f5a7a224 Mon Sep 17 00:00:00 2001 From: Jack Draper Date: Sat, 11 Nov 2023 08:38:28 +0000 Subject: [PATCH 06/16] fix: remove all twitch alert fks --- alembic/versions/dd3c60f39768_mysql_migration.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/alembic/versions/dd3c60f39768_mysql_migration.py b/alembic/versions/dd3c60f39768_mysql_migration.py index db09b3f3..a0348efa 100644 --- a/alembic/versions/dd3c60f39768_mysql_migration.py +++ b/alembic/versions/dd3c60f39768_mysql_migration.py @@ -228,12 +228,14 @@ def unsafe_upgrade(): 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', Column('team_twitch_alert_id', INT, - ForeignKey("TeamInTwitchAlert.team_twitch_alert_id", ondelete='CASCADE'), + # ForeignKey("TeamInTwitchAlert.team_twitch_alert_id", ondelete='CASCADE'), primary_key=True), Column('twitch_username', VARCHAR(25), primary_key=True), Column('message_id', DiscordSnowflake, nullable=True)) From 5964da0b69a368723182cc402b3f9670154ca458 Mon Sep 17 00:00:00 2001 From: Jack Draper Date: Sat, 11 Nov 2023 08:44:52 +0000 Subject: [PATCH 07/16] fix: increase email size --- alembic/versions/dd3c60f39768_mysql_migration.py | 4 ++-- koala/cogs/verification/models.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/alembic/versions/dd3c60f39768_mysql_migration.py b/alembic/versions/dd3c60f39768_mysql_migration.py index a0348efa..1e34f7e3 100644 --- a/alembic/versions/dd3c60f39768_mysql_migration.py +++ b/alembic/versions/dd3c60f39768_mysql_migration.py @@ -242,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), 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): From 95e32408603864a5f55c95724a604c078e06a913 Mon Sep 17 00:00:00 2001 From: Jack Draper Date: Sat, 11 Nov 2023 09:34:43 +0000 Subject: [PATCH 08/16] fix: rfr doesn't assign roles --- alembic/versions/dd3c60f39768_mysql_migration.py | 6 +++--- koala/cogs/react_for_role/cog.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/alembic/versions/dd3c60f39768_mysql_migration.py b/alembic/versions/dd3c60f39768_mysql_migration.py index 1e34f7e3..165f97d1 100644 --- a/alembic/versions/dd3c60f39768_mysql_migration.py +++ b/alembic/versions/dd3c60f39768_mysql_migration.py @@ -220,7 +220,7 @@ 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'), + 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), @@ -229,13 +229,13 @@ def unsafe_upgrade(): Column('team_twitch_alert_id', INT, autoincrement=True, primary_key=True), Column('channel_id', DiscordSnowflake, - # ForeignKey("TwitchAlerts.channel_id", ondelete='CASCADE') + 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', Column('team_twitch_alert_id', INT, - # ForeignKey("TeamInTwitchAlert.team_twitch_alert_id", ondelete='CASCADE'), + ForeignKey("TeamInTwitchAlert.team_twitch_alert_id", ondelete='CASCADE'), primary_key=True), Column('twitch_username', VARCHAR(25), primary_key=True), Column('message_id', DiscordSnowflake, nullable=True)) diff --git a/koala/cogs/react_for_role/cog.py b/koala/cogs/react_for_role/cog.py index 16907908..ec0cdc92 100644 --- a/koala/cogs/react_for_role/cog.py +++ b/koala/cogs/react_for_role/cog.py @@ -23,6 +23,7 @@ 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 @@ -730,7 +731,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) From 8116aba221ef04c7307caf6b0a4d550a92eca15e Mon Sep 17 00:00:00 2001 From: Jack Draper Date: Wed, 17 Jul 2024 19:09:08 +0100 Subject: [PATCH 09/16] feat: passthrough kb2 /koala/support --- devdocs/KB2.md | 27 + dislord/__init__.py | 10 + dislord/api.py | 71 ++ dislord/client.py | 179 ++++ dislord/discord/__init__.py | 1 + .../discord/interactions}/__init__.py | 0 .../application_commands}/__init__.py | 0 .../application_commands/enums.py | 44 + .../application_commands/models.py | 69 ++ .../interactions/components}/__init__.py | 0 .../discord/interactions/components/enums.py | 25 + .../discord/interactions/components/models.py | 60 ++ .../receiving_and_responding}/__init__.py | 0 .../receiving_and_responding/interaction.py | 66 ++ .../interaction_data.py | 54 ++ .../interaction_response.py | 61 ++ .../message_interaction.py | 22 + dislord/discord/reference.py | 63 ++ .../discord/resources}/__init__.py | 0 .../resources/application}/__init__.py | 0 .../discord/resources/application/enums.py | 6 + .../discord/resources/application/flags.py | 14 + .../discord/resources/application/models.py | 49 + .../__init__.py | 0 .../enums.py | 12 + .../models.py | 12 + .../discord/resources/audit_log}/__init__.py | 0 dislord/discord/resources/audit_log/enums.py | 69 ++ dislord/discord/resources/audit_log/models.py | 48 + .../resources/auto_moderation}/__init__.py | 0 .../resources/auto_moderation/enums.py | 24 + .../resources/auto_moderation/models.py | 36 + .../discord/resources/channel}/__init__.py | 0 .../resources/channel/allowed_mentions.py | 17 + .../discord/resources/channel/attachment.py | 27 + dislord/discord/resources/channel/channel.py | 89 ++ .../resources/channel/channel_mention.py | 10 + .../resources/channel/default_reaction.py | 8 + dislord/discord/resources/channel/embed.py | 63 ++ dislord/discord/resources/channel/enums.py | 5 + .../resources/channel/followed_channel.py | 7 + .../discord/resources/channel/forum_tag.py | 10 + dislord/discord/resources/channel/message.py | 66 ++ .../discord/resources/channel/message_call.py | 7 + .../channel/message_interaction_metadata.py | 17 + .../resources/channel/message_reference.py | 9 + .../discord/resources/channel/overwrite.py | 16 + .../resources/channel/partial_message.py | 61 ++ dislord/discord/resources/channel/reaction.py | 13 + .../channel/reaction_count_details.py | 6 + .../channel/role_subscription_data.py | 10 + .../resources/channel/thread_member.py | 13 + .../resources/channel/thread_metadata.py | 11 + .../discord/resources/emoji}/__init__.py | 0 dislord/discord/resources/emoji/emoji.py | 17 + dislord/discord/resources/guild/__init__.py | 0 dislord/discord/resources/guild/ban.py | 8 + dislord/discord/resources/guild/guild.py | 141 +++ .../discord/resources/guild/guild_member.py | 32 + .../resources/guild/guild_onboarding.py | 45 + .../discord/resources/guild/guild_preview.py | 19 + .../discord/resources/guild/guild_widget.py | 13 + .../resources/guild/guild_widget_settings.py | 7 + .../discord/resources/guild/integration.py | 31 + .../resources/guild/integration_account.py | 6 + .../guild/integration_application.py | 11 + .../discord/resources/guild/welcome_screen.py | 14 + .../guild_scheduled_event/__init__.py | 0 .../guild_scheduled_event.py | 45 + .../guild_scheduled_event_user.py | 10 + .../resources/guild_template/__init__.py | 0 .../guild_template/guild_template.py | 18 + dislord/discord/resources/invite/__init__.py | 0 dislord/discord/resources/invite/invite.py | 37 + .../resources/invite/invite_metadata.py | 10 + .../resources/invite/invite_stage_instance.py | 9 + dislord/discord/resources/poll/__init__.py | 0 dislord/discord/resources/poll/layout_type.py | 5 + dislord/discord/resources/poll/poll.py | 15 + dislord/discord/resources/poll/poll_answer.py | 7 + .../resources/poll/poll_create_request.py | 13 + dislord/discord/resources/poll/poll_media.py | 8 + .../discord/resources/poll/poll_results.py | 12 + .../resources/stage_instance/__init__.py | 0 .../stage_instance/stage_instance.py | 17 + dislord/discord/resources/sticker/__init__.py | 0 dislord/discord/resources/sticker/sticker.py | 32 + .../discord/resources/sticker/sticker_item.py | 9 + .../discord/resources/sticker/sticker_pack.py | 13 + dislord/discord/resources/user/__init__.py | 0 .../user/application_role_connection.py | 8 + .../resources/user/avatar_decoration_data.py | 7 + dislord/discord/resources/user/connection.py | 23 + dislord/discord/resources/user/user.py | 52 + dislord/discord/resources/voice/__init__.py | 0 .../discord/resources/voice/voice_region.py | 9 + .../discord/resources/voice/voice_state.py | 19 + dislord/discord/resources/webhook/__init__.py | 0 dislord/discord/resources/webhook/webhook.py | 27 + dislord/discord/topics/__init__.py | 0 .../discord/topics/permissions/__init__.py | 0 .../discord/topics/permissions/permissions.py | 52 + dislord/discord/topics/permissions/role.py | 32 + dislord/discord/topics/teams/__init__.py | 0 dislord/discord/topics/teams/data_models.py | 26 + .../discord/topics/teams/team_member_roles.py | 8 + dislord/dpyadapter/__init__.py | 1 + dislord/dpyadapter/handlers.py | 15 + dislord/error.py | 6 + dislord/group.py | 47 + dislord/model/__init__.py | 0 dislord/model/api.py | 30 + dislord/model/application.py | 13 + dislord/model/base.py | 242 +++++ dislord/model/channel.py | 12 + dislord/model/commands.py | 35 + dislord/model/components.py | 0 dislord/model/guild.py | 13 + dislord/model/user.py | 14 + dislord/server.py | 42 + dislord/types.py | 18 + kb2/__init__.py | 1 + kb2/ext/__init__.py | 1 + kb2/ext/koala/__init__.py | 1 + kb2/ext/koala/api.py | 0 kb2/ext/koala/commands.py | 15 + kb2/ext/koala/core.py | 4 + kb2/ext/verify/__init__.py | 0 kb2/ext/verify/commands.py | 5 + kb2/ext/verifyme/__init__.py | 0 kb2/ext/verifyme/commands.py | 11 + kb2/main.py | 23 + koala/cogs/base/cog.py | 23 - koala/cogs/verification/api.py | 2 +- koalabot.py | 44 +- requirements.txt | 3 + tests/conftest.py | 27 - tests/kb2/__init__.py | 0 tests/kb2/ext/__init__.py | 0 tests/kb2/ext/koala/__init__.py | 0 tests/kb2/ext/koala/test_commands.py | 8 + tests/koala/__init__.py | 0 tests/koala/cogs/__init__.py | 0 tests/koala/cogs/announce/__init__.py | 0 tests/{ => koala}/cogs/announce/test_cog.py | 2 +- tests/koala/cogs/base/__init__.py | 0 tests/{ => koala}/cogs/base/conftest.py | 0 tests/{ => koala}/cogs/base/test_api.py | 896 +++++++++--------- tests/{ => koala}/cogs/base/test_cog.py | 22 +- tests/{ => koala}/cogs/base/test_core.py | 0 tests/{ => koala}/cogs/base/test_db.py | 0 tests/koala/cogs/colour_role/__init__.py | 0 .../{ => koala}/cogs/colour_role/test_cog.py | 2 +- tests/{ => koala}/cogs/colour_role/test_db.py | 0 tests/{ => koala}/cogs/colour_role/utils.py | 0 tests/koala/cogs/insights/__init__.py | 0 tests/{ => koala}/cogs/insights/test_cog.py | 0 tests/koala/cogs/intro_cog/__init__.py | 0 tests/{ => koala}/cogs/intro_cog/conftest.py | 2 +- tests/{ => koala}/cogs/intro_cog/test_cog.py | 0 tests/{ => koala}/cogs/intro_cog/test_db.py | 0 .../{ => koala}/cogs/intro_cog/test_utils.py | 0 tests/{ => koala}/cogs/intro_cog/utils.py | 0 tests/koala/cogs/react_for_role/__init__.py | 0 .../cogs/react_for_role/conftest.py | 2 +- .../cogs/react_for_role/test_api.py | 0 .../cogs/react_for_role/test_cog.py | 6 +- .../cogs/react_for_role/test_db.py | 2 +- .../{ => koala}/cogs/react_for_role/utils.py | 0 tests/koala/cogs/text_filter/__init__.py | 0 .../{ => koala}/cogs/text_filter/test_cog.py | 2 +- tests/koala/cogs/twitch_alert/__init__.py | 0 .../{ => koala}/cogs/twitch_alert/test_cog.py | 2 +- .../{ => koala}/cogs/twitch_alert/test_db.py | 0 .../cogs/twitch_alert/test_twitch_handler.py | 0 .../cogs/twitch_alert/test_utils.py | 0 tests/koala/cogs/verification/__init__.py | 0 .../{ => koala}/cogs/verification/test_api.py | 0 .../{ => koala}/cogs/verification/test_cog.py | 0 .../cogs/verification/test_core.py | 0 tests/koala/cogs/voting/__init__.py | 0 tests/{ => koala}/cogs/voting/conftest.py | 0 tests/{ => koala}/cogs/voting/test_cog.py | 0 tests/{ => koala}/cogs/voting/test_db.py | 0 tests/{ => koala}/cogs/voting/test_option.py | 0 tests/{ => koala}/cogs/voting/test_two_way.py | 0 tests/{ => koala}/cogs/voting/test_vote.py | 0 tests/{ => koala}/cogs/voting/utils.py | 0 tests/koala/conftest.py | 41 + tests/{ => koala}/test_koalabot.py | 282 +++--- tests/{ => koala}/test_utils.py | 2 +- tests/{ => koala}/tests_utils/__init__.py | 0 .../fake_load_all_cogs/__init__.py | 0 .../fake_load_all_cogs/greetings_cog.py | 0 tests/{ => koala}/tests_utils/last_ctx_cog.py | 0 tests/{ => koala}/tests_utils/utils.py | 0 196 files changed, 3601 insertions(+), 672 deletions(-) create mode 100644 devdocs/KB2.md create mode 100644 dislord/__init__.py create mode 100644 dislord/api.py create mode 100644 dislord/client.py create mode 100644 dislord/discord/__init__.py rename {tests/cogs => dislord/discord/interactions}/__init__.py (100%) rename {tests/cogs/announce => dislord/discord/interactions/application_commands}/__init__.py (100%) create mode 100644 dislord/discord/interactions/application_commands/enums.py create mode 100644 dislord/discord/interactions/application_commands/models.py rename {tests/cogs/base => dislord/discord/interactions/components}/__init__.py (100%) create mode 100644 dislord/discord/interactions/components/enums.py create mode 100644 dislord/discord/interactions/components/models.py rename {tests/cogs/colour_role => dislord/discord/interactions/receiving_and_responding}/__init__.py (100%) create mode 100644 dislord/discord/interactions/receiving_and_responding/interaction.py create mode 100644 dislord/discord/interactions/receiving_and_responding/interaction_data.py create mode 100644 dislord/discord/interactions/receiving_and_responding/interaction_response.py create mode 100644 dislord/discord/interactions/receiving_and_responding/message_interaction.py create mode 100644 dislord/discord/reference.py rename {tests/cogs/insights => dislord/discord/resources}/__init__.py (100%) rename {tests/cogs/intro_cog => dislord/discord/resources/application}/__init__.py (100%) create mode 100644 dislord/discord/resources/application/enums.py create mode 100644 dislord/discord/resources/application/flags.py create mode 100644 dislord/discord/resources/application/models.py rename {tests/cogs/react_for_role => dislord/discord/resources/application_role_connection_metadata}/__init__.py (100%) create mode 100644 dislord/discord/resources/application_role_connection_metadata/enums.py create mode 100644 dislord/discord/resources/application_role_connection_metadata/models.py rename {tests/cogs/text_filter => dislord/discord/resources/audit_log}/__init__.py (100%) create mode 100644 dislord/discord/resources/audit_log/enums.py create mode 100644 dislord/discord/resources/audit_log/models.py rename {tests/cogs/twitch_alert => dislord/discord/resources/auto_moderation}/__init__.py (100%) create mode 100644 dislord/discord/resources/auto_moderation/enums.py create mode 100644 dislord/discord/resources/auto_moderation/models.py rename {tests/cogs/verification => dislord/discord/resources/channel}/__init__.py (100%) create mode 100644 dislord/discord/resources/channel/allowed_mentions.py create mode 100644 dislord/discord/resources/channel/attachment.py create mode 100644 dislord/discord/resources/channel/channel.py create mode 100644 dislord/discord/resources/channel/channel_mention.py create mode 100644 dislord/discord/resources/channel/default_reaction.py create mode 100644 dislord/discord/resources/channel/embed.py create mode 100644 dislord/discord/resources/channel/enums.py create mode 100644 dislord/discord/resources/channel/followed_channel.py create mode 100644 dislord/discord/resources/channel/forum_tag.py create mode 100644 dislord/discord/resources/channel/message.py create mode 100644 dislord/discord/resources/channel/message_call.py create mode 100644 dislord/discord/resources/channel/message_interaction_metadata.py create mode 100644 dislord/discord/resources/channel/message_reference.py create mode 100644 dislord/discord/resources/channel/overwrite.py create mode 100644 dislord/discord/resources/channel/partial_message.py create mode 100644 dislord/discord/resources/channel/reaction.py create mode 100644 dislord/discord/resources/channel/reaction_count_details.py create mode 100644 dislord/discord/resources/channel/role_subscription_data.py create mode 100644 dislord/discord/resources/channel/thread_member.py create mode 100644 dislord/discord/resources/channel/thread_metadata.py rename {tests/cogs/voting => dislord/discord/resources/emoji}/__init__.py (100%) create mode 100644 dislord/discord/resources/emoji/emoji.py create mode 100644 dislord/discord/resources/guild/__init__.py create mode 100644 dislord/discord/resources/guild/ban.py create mode 100644 dislord/discord/resources/guild/guild.py create mode 100644 dislord/discord/resources/guild/guild_member.py create mode 100644 dislord/discord/resources/guild/guild_onboarding.py create mode 100644 dislord/discord/resources/guild/guild_preview.py create mode 100644 dislord/discord/resources/guild/guild_widget.py create mode 100644 dislord/discord/resources/guild/guild_widget_settings.py create mode 100644 dislord/discord/resources/guild/integration.py create mode 100644 dislord/discord/resources/guild/integration_account.py create mode 100644 dislord/discord/resources/guild/integration_application.py create mode 100644 dislord/discord/resources/guild/welcome_screen.py create mode 100644 dislord/discord/resources/guild_scheduled_event/__init__.py create mode 100644 dislord/discord/resources/guild_scheduled_event/guild_scheduled_event.py create mode 100644 dislord/discord/resources/guild_scheduled_event/guild_scheduled_event_user.py create mode 100644 dislord/discord/resources/guild_template/__init__.py create mode 100644 dislord/discord/resources/guild_template/guild_template.py create mode 100644 dislord/discord/resources/invite/__init__.py create mode 100644 dislord/discord/resources/invite/invite.py create mode 100644 dislord/discord/resources/invite/invite_metadata.py create mode 100644 dislord/discord/resources/invite/invite_stage_instance.py create mode 100644 dislord/discord/resources/poll/__init__.py create mode 100644 dislord/discord/resources/poll/layout_type.py create mode 100644 dislord/discord/resources/poll/poll.py create mode 100644 dislord/discord/resources/poll/poll_answer.py create mode 100644 dislord/discord/resources/poll/poll_create_request.py create mode 100644 dislord/discord/resources/poll/poll_media.py create mode 100644 dislord/discord/resources/poll/poll_results.py create mode 100644 dislord/discord/resources/stage_instance/__init__.py create mode 100644 dislord/discord/resources/stage_instance/stage_instance.py create mode 100644 dislord/discord/resources/sticker/__init__.py create mode 100644 dislord/discord/resources/sticker/sticker.py create mode 100644 dislord/discord/resources/sticker/sticker_item.py create mode 100644 dislord/discord/resources/sticker/sticker_pack.py create mode 100644 dislord/discord/resources/user/__init__.py create mode 100644 dislord/discord/resources/user/application_role_connection.py create mode 100644 dislord/discord/resources/user/avatar_decoration_data.py create mode 100644 dislord/discord/resources/user/connection.py create mode 100644 dislord/discord/resources/user/user.py create mode 100644 dislord/discord/resources/voice/__init__.py create mode 100644 dislord/discord/resources/voice/voice_region.py create mode 100644 dislord/discord/resources/voice/voice_state.py create mode 100644 dislord/discord/resources/webhook/__init__.py create mode 100644 dislord/discord/resources/webhook/webhook.py create mode 100644 dislord/discord/topics/__init__.py create mode 100644 dislord/discord/topics/permissions/__init__.py create mode 100644 dislord/discord/topics/permissions/permissions.py create mode 100644 dislord/discord/topics/permissions/role.py create mode 100644 dislord/discord/topics/teams/__init__.py create mode 100644 dislord/discord/topics/teams/data_models.py create mode 100644 dislord/discord/topics/teams/team_member_roles.py create mode 100644 dislord/dpyadapter/__init__.py create mode 100644 dislord/dpyadapter/handlers.py create mode 100644 dislord/error.py create mode 100644 dislord/group.py create mode 100644 dislord/model/__init__.py create mode 100644 dislord/model/api.py create mode 100644 dislord/model/application.py create mode 100644 dislord/model/base.py create mode 100644 dislord/model/channel.py create mode 100644 dislord/model/commands.py create mode 100644 dislord/model/components.py create mode 100644 dislord/model/guild.py create mode 100644 dislord/model/user.py create mode 100644 dislord/server.py create mode 100644 dislord/types.py create mode 100644 kb2/__init__.py create mode 100644 kb2/ext/__init__.py create mode 100644 kb2/ext/koala/__init__.py create mode 100644 kb2/ext/koala/api.py create mode 100644 kb2/ext/koala/commands.py create mode 100644 kb2/ext/koala/core.py create mode 100644 kb2/ext/verify/__init__.py create mode 100644 kb2/ext/verify/commands.py create mode 100644 kb2/ext/verifyme/__init__.py create mode 100644 kb2/ext/verifyme/commands.py create mode 100644 kb2/main.py create mode 100644 tests/kb2/__init__.py create mode 100644 tests/kb2/ext/__init__.py create mode 100644 tests/kb2/ext/koala/__init__.py create mode 100644 tests/kb2/ext/koala/test_commands.py create mode 100644 tests/koala/__init__.py create mode 100644 tests/koala/cogs/__init__.py create mode 100644 tests/koala/cogs/announce/__init__.py rename tests/{ => koala}/cogs/announce/test_cog.py (99%) create mode 100644 tests/koala/cogs/base/__init__.py rename tests/{ => koala}/cogs/base/conftest.py (100%) rename tests/{ => koala}/cogs/base/test_api.py (96%) rename tests/{ => koala}/cogs/base/test_cog.py (91%) rename tests/{ => koala}/cogs/base/test_core.py (100%) rename tests/{ => koala}/cogs/base/test_db.py (100%) create mode 100644 tests/koala/cogs/colour_role/__init__.py rename tests/{ => koala}/cogs/colour_role/test_cog.py (99%) rename tests/{ => koala}/cogs/colour_role/test_db.py (100%) rename tests/{ => koala}/cogs/colour_role/utils.py (100%) create mode 100644 tests/koala/cogs/insights/__init__.py rename tests/{ => koala}/cogs/insights/test_cog.py (100%) create mode 100644 tests/koala/cogs/intro_cog/__init__.py rename tests/{ => koala}/cogs/intro_cog/conftest.py (92%) rename tests/{ => koala}/cogs/intro_cog/test_cog.py (100%) rename tests/{ => koala}/cogs/intro_cog/test_db.py (100%) rename tests/{ => koala}/cogs/intro_cog/test_utils.py (100%) rename tests/{ => koala}/cogs/intro_cog/utils.py (100%) create mode 100644 tests/koala/cogs/react_for_role/__init__.py rename tests/{ => koala}/cogs/react_for_role/conftest.py (95%) rename tests/{ => koala}/cogs/react_for_role/test_api.py (100%) rename tests/{ => koala}/cogs/react_for_role/test_cog.py (99%) rename tests/{ => koala}/cogs/react_for_role/test_db.py (99%) rename tests/{ => koala}/cogs/react_for_role/utils.py (100%) create mode 100644 tests/koala/cogs/text_filter/__init__.py rename tests/{ => koala}/cogs/text_filter/test_cog.py (99%) create mode 100644 tests/koala/cogs/twitch_alert/__init__.py rename tests/{ => koala}/cogs/twitch_alert/test_cog.py (99%) rename tests/{ => koala}/cogs/twitch_alert/test_db.py (100%) rename tests/{ => koala}/cogs/twitch_alert/test_twitch_handler.py (100%) rename tests/{ => koala}/cogs/twitch_alert/test_utils.py (100%) create mode 100644 tests/koala/cogs/verification/__init__.py rename tests/{ => koala}/cogs/verification/test_api.py (100%) rename tests/{ => koala}/cogs/verification/test_cog.py (100%) rename tests/{ => koala}/cogs/verification/test_core.py (100%) create mode 100644 tests/koala/cogs/voting/__init__.py rename tests/{ => koala}/cogs/voting/conftest.py (100%) rename tests/{ => koala}/cogs/voting/test_cog.py (100%) rename tests/{ => koala}/cogs/voting/test_db.py (100%) rename tests/{ => koala}/cogs/voting/test_option.py (100%) rename tests/{ => koala}/cogs/voting/test_two_way.py (100%) rename tests/{ => koala}/cogs/voting/test_vote.py (100%) rename tests/{ => koala}/cogs/voting/utils.py (100%) create mode 100644 tests/koala/conftest.py rename tests/{ => koala}/test_koalabot.py (89%) rename tests/{ => koala}/test_utils.py (97%) rename tests/{ => koala}/tests_utils/__init__.py (100%) rename tests/{ => koala}/tests_utils/fake_load_all_cogs/__init__.py (100%) rename tests/{ => koala}/tests_utils/fake_load_all_cogs/greetings_cog.py (100%) rename tests/{ => koala}/tests_utils/last_ctx_cog.py (100%) rename tests/{ => koala}/tests_utils/utils.py (100%) diff --git a/devdocs/KB2.md b/devdocs/KB2.md new file mode 100644 index 00000000..3c6f0e8b --- /dev/null +++ b/devdocs/KB2.md @@ -0,0 +1,27 @@ +# KoalaBot 2.0 (KB2) +## Architecture +Migrating to Serverless Architecture utilising DynamoDB. + +# Base +## Bot Owner +- `/owner koala reload` (v1 only) - Reload discord.py extensions +- `/owner koala ping` - Get ping +- `/owner koala activity` - Manage current and scheduled + +## Guild Admin +- `/koala extension` - show and enable/disable extensions +- `/koala clear ` - Delete messages in the channel + +## Guild Member +- `/koala support` +- `/koala version` + +# Verify +## Bot Owner +- `/owner verify emails ` + +## Guild Admin +- `/verify enable` + +## Guild Member +- `/verifyme` \ No newline at end of file diff --git a/dislord/__init__.py b/dislord/__init__.py new file mode 100644 index 00000000..a2fc5a3d --- /dev/null +++ b/dislord/__init__.py @@ -0,0 +1,10 @@ +from . import api, client, error, server +from .discord import interactions +from .client import ApplicationClient +from .group import CommandGroup + + +try: + from . import dpyadapter +except ImportError: + pass # dpy not configured diff --git a/dislord/api.py b/dislord/api.py new file mode 100644 index 00000000..6878eb15 --- /dev/null +++ b/dislord/api.py @@ -0,0 +1,71 @@ +import json +from time import sleep + +import requests + +from .discord.reference import DISCORD_URL +from .error import DiscordApiException +from .model.base import cast, EnhancedJSONEncoder + + +# WARNING: Average time to call and get response from API is 25ms, not great to call lots if you want quick processing + + +class DiscordApi: + def __init__(self, client, bot_token): + self.client = client + self.bot_token = bot_token + self.auth_header = {"Authorization": "Bot " + self.bot_token} + + def get(self, endpoint: str, params: dict = None, type_hint: type = None, **kwargs): + print(f"📨 Sending to Discord API GET: {endpoint}, {params}") + response = requests.get(DISCORD_URL + endpoint, params, **kwargs, headers=self.auth_header) + if response.ok: + print(f"📬 Response from Discord API: {response.content}") + response_payload = json.loads(response.content) + if type_hint: + return cast(response_payload, type_hint, client=self.client) + else: + return response_payload + elif response.status_code == 429: + retry_after = response.json()["retry_after"] + print(f"⚠️ Rate Limited, waiting {retry_after}s") + sleep(retry_after) + return self.get(endpoint, params, type_hint, **kwargs) + else: + raise DiscordApiException(f"{response.status_code} {response.text} error when calling discord API " + f"URL: GET {endpoint} Params: {params}") + + def delete(self, endpoint: str, **kwargs): + print(f"📨 Sending to Discord API DELETE: {endpoint}") + response = requests.delete(DISCORD_URL + endpoint, **kwargs, headers=self.auth_header) + if response.ok: + print(f"📬 Response from Discord API: {response.content}") + return + elif response.status_code == 429: + retry_after = response.json()["retry_after"] + print(f"⚠️ Rate Limited, waiting {retry_after}s") + sleep(retry_after) + return self.delete(endpoint, **kwargs) + else: + raise DiscordApiException(f"{response.status_code} {response.text} error when calling discord API " + f"URL: DELETE {endpoint}") + + def post(self, endpoint: str, body: object = None, type_hint: type = None, **kwargs): + body_json = json.dumps(body, cls=EnhancedJSONEncoder) + print(f"📨 Sending to Discord API POST: {endpoint}, {body_json}") + headers = self.auth_header + headers["Content-Type"] = "application/json" + response = requests.post(DISCORD_URL + endpoint, data=body_json, + **kwargs, headers=headers) + if response.ok: + print(f"📬 Response from Discord API: {response.content}") + return cast(json.loads(response.content), type_hint, client=self.client) + elif response.status_code == 429: + retry_after = response.json()["retry_after"] + print(f"⚠️ Rate Limited, waiting {retry_after}s") + sleep(retry_after) + return self.post(endpoint, body, type_hint, **kwargs) + else: + raise DiscordApiException(f"{response.status_code} {response.text} error when calling discord API " + f"URL: POST {endpoint} Body: {body_json}") diff --git a/dislord/client.py b/dislord/client.py new file mode 100644 index 00000000..c088f5f5 --- /dev/null +++ b/dislord/client.py @@ -0,0 +1,179 @@ +import json +from typing import Callable + +from discord_interactions import verify_key, InteractionType + +from .discord.interactions.application_commands.enums import ApplicationCommandType +from .discord.interactions.application_commands.models import ApplicationCommandOption +from .discord.interactions.receiving_and_responding.interaction import Interaction +from .discord.interactions.receiving_and_responding.interaction_response import InteractionResponse +from .discord.reference import Snowflake, Missing +from .discord.resources.application.models import Application +from .group import CommandGroup +from .api import DiscordApi +from .error import DiscordApiException +from .model.api import HttpResponse, HttpUnauthorized, HttpOk +from .model.base import cast, EnhancedJSONEncoder +from .model.channel import Channel +from .model.commands import ApplicationCommand +from .model.guild import Guild +from .model.user import User + + +class ApplicationClient: + _public_key: str + _api: DiscordApi + _commands: dict[Snowflake, dict[str, ApplicationCommand]] = {} + _command_callbacks: dict[str, Callable] = {} + _application: Application = Missing() + _guilds: list[Guild] = Missing() + + def __init__(self, public_key, bot_token): + self._public_key = public_key + self._api = DiscordApi(self, bot_token) + + def verified_interact(self, raw_request, signature, timestamp) -> HttpResponse: + if signature is None or timestamp is None or not verify_key(json.dumps(raw_request, separators=(',', ':')) + .encode('utf-8'), signature, timestamp, + self._public_key): + return HttpUnauthorized('Bad request signature') + return self.interact(raw_request) + + def interact(self, raw_request) -> HttpResponse: + interaction = cast(raw_request, Interaction, self) + + if interaction.type == InteractionType.PING: # PING + response_data = InteractionResponse.pong() # PONG + elif interaction.type == InteractionType.APPLICATION_COMMAND: + data = interaction.data + command_name = data.name + kwargs = {} + for option in data.options: + kwargs[option.name] = option.value + response_data = self._command_callbacks[command_name](interaction=interaction, **kwargs) + + else: + raise DiscordApiException(DiscordApiException.UNKNOWN_INTERACTION_TYPE.format(interaction.type)) + + return HttpOk(json.loads(json.dumps(response_data, cls=EnhancedJSONEncoder)), headers={"Content-Type": "application/json"}) + + def add_command(self, command: ApplicationCommand, callback: Callable): + if self._commands.get(command.guild_id) is None: + self._commands[command.guild_id] = {} + self._command_callbacks[command.name] = callback + self._commands.get(command.guild_id)[command.name] = command + + def command(self, *, name, description, dm_permission=True, nsfw=False, guild_ids: list[Snowflake] = None, + options: list[ApplicationCommandOption] = None): + if guild_ids is None: + guild_ids = ["ALL"] + + def decorator(func): + for guild_id in guild_ids: + if guild_id == "ALL": + guild_id = None + self.add_command(ApplicationCommand(name=name, description=description, + type=ApplicationCommandType.CHAT_INPUT, + dm_permission=dm_permission, nsfw=nsfw, + guild_id=guild_id, options=options, client=self), func) + return func + + return decorator + + def register_group(self, command_group: CommandGroup): + if self._commands.get(command_group.guild_id) is None: + self._commands[command_group.guild_id] = {} + + self._commands[command_group.guild_id][command_group.name] = ApplicationCommand( + name=command_group.name, description=command_group.description, type=ApplicationCommandType.CHAT_INPUT, + dm_permission=command_group.dm_permission, nsfw=command_group.nsfw, guild_id=command_group.guild_id, + options=list(command_group.commands.values()), client=self) + + self._command_callbacks[command_group.name] = command_group.callback + + def serverless_handler(self, event, context): + if event['httpMethod'] == "POST": + print(f"🫱 Full Event: {event}") + raw_request = json.loads(event["body"]) + print(f"👉 Request: {raw_request}") + raw_headers = event["headers"] + signature = raw_headers.get('x-signature-ed25519') + timestamp = raw_headers.get('x-signature-timestamp') + response = self.verified_interact(raw_request, signature, timestamp).as_serverless_response() + print(f"🫴 Response: {response}") + return response + + @property + def application(self): + if self._application is Missing(): + self._application = self.get_application() + return self._application + + @property + def guilds(self) -> list[Guild]: + if self._guilds is Missing(): + self._guilds = self._get_guilds() + return self._guilds + + def get_application(self): + return self._api.get("/applications/@me", type_hint=Application) + + def sync_commands(self, guild_id: Snowflake = None, guild_ids: list[Snowflake] = None, + application_id: Snowflake = None): + if guild_ids: + for g_id in guild_ids: + self.sync_commands(guild_id=g_id, application_id=application_id) + + registered_commands = self._get_commands(guild_id) + client_commands = self._commands.get(guild_id) + missing_commands = list(client_commands.values()) if client_commands else [] + for registered_command in registered_commands: + if registered_command not in missing_commands: + self._delete_commands(command_id=registered_command.id, guild_id=guild_id, + application_id=registered_command.application_id) + else: + missing_commands.remove(registered_command) + + for missing_command in missing_commands: + self._register_command(missing_command, guild_id=guild_id, application_id=application_id) + + def _get_commands(self, guild_id: Snowflake = None, application_id: Snowflake = None, + with_localizations: bool = None) -> list[ApplicationCommand]: + endpoint = f"/applications/{application_id if application_id else self.application.id}" + if guild_id: + endpoint += f"/guilds/{guild_id}" + + params = {} + if with_localizations is not None: + params["with_localizations"] = with_localizations + + return [ApplicationCommand(**p) for p in self._api.get(f"{endpoint}/commands", params=params, + type_hint=list[ApplicationCommand])] + + def _delete_commands(self, command_id: Snowflake, + guild_id: Snowflake = None, application_id: Snowflake = None) -> None: + endpoint = f"/applications/{application_id if application_id else self.application.id}" + if guild_id: + endpoint += f"/guilds/{guild_id}" + + self._api.delete(f"{endpoint}/commands/{command_id}") + + def _register_command(self, application_command: ApplicationCommand, + guild_id: Snowflake = None, application_id: Snowflake = None) -> ApplicationCommand: + endpoint = f"/applications/{application_id if application_id else self.application.id}" + if guild_id: + endpoint += f"/guilds/{guild_id}" + return ApplicationCommand(**self._api.post(f"{endpoint}/commands", application_command.to_payload(), + type_hint=ApplicationCommand)) + + def get_user(self, user_id=None) -> User: + return User.from_payload(self._api.get(f"/users/{user_id if user_id else '@me'}")) + + def get_guild(self, guild_id) -> Guild: + return Guild.from_payload(self._api.get(f"/guilds/{guild_id}")) + + def _get_guilds(self) -> list[Guild]: + return [Guild.from_payload(p) for p in self._api.get("/users/@me/guilds")] + + def get_channel(self, channel_id) -> list[Channel]: + return [Channel.from_payload(p) for p in self._api.get(f"/channels/{channel_id}")] diff --git a/dislord/discord/__init__.py b/dislord/discord/__init__.py new file mode 100644 index 00000000..b6e690fd --- /dev/null +++ b/dislord/discord/__init__.py @@ -0,0 +1 @@ +from . import * diff --git a/tests/cogs/__init__.py b/dislord/discord/interactions/__init__.py similarity index 100% rename from tests/cogs/__init__.py rename to dislord/discord/interactions/__init__.py diff --git a/tests/cogs/announce/__init__.py b/dislord/discord/interactions/application_commands/__init__.py similarity index 100% rename from tests/cogs/announce/__init__.py rename to dislord/discord/interactions/application_commands/__init__.py diff --git a/dislord/discord/interactions/application_commands/enums.py b/dislord/discord/interactions/application_commands/enums.py new file mode 100644 index 00000000..af263096 --- /dev/null +++ b/dislord/discord/interactions/application_commands/enums.py @@ -0,0 +1,44 @@ +from enum import Enum + + +class ApplicationCommandType(Enum): + CHAT_INPUT = 1 + USER = 2 + MESSAGE = 3 + + +class ApplicationCommandOptionType(Enum): + SUB_COMMAND = 1 + SUB_COMMAND_GROUP = 2 + STRING = 3 + INTEGER = 4 + BOOLEAN = 5 + USER = 6 + CHANNEL = 7 + ROLE = 8 + MENTIONABLE = 9 + NUMBER = 10 + ATTACHMENT = 11 + + # def from_python_type(self, type_hint): + # python_mapping = {str: self.STRING, int: self.INTEGER, bool: self.BOOLEAN, + # User: self.USER, Channel: self.CHANNEL, + # # Role: self.ROLE, Mentionable: self.MENTIONABLE, FIXME + # float: self.NUMBER, + # # Attachment: self.ATTACHMENT FIXME + # } + # par = python_mapping.get(type_hint) + # if par is None: + # raise RuntimeError(f"Unexpected command param type: {type_hint}") + # + # def __eq__(self, other): + # try: + # return self.value == other.value + # except Exception: + # return False + + +class ApplicationCommandPermissionType(Enum): + ROLE = 1 + USER = 2 + CHANNEL = 3 diff --git a/dislord/discord/interactions/application_commands/models.py b/dislord/discord/interactions/application_commands/models.py new file mode 100644 index 00000000..1294341c --- /dev/null +++ b/dislord/discord/interactions/application_commands/models.py @@ -0,0 +1,69 @@ +from typing import Self, TypedDict + +from discord import Permissions + +from dislord.discord.interactions.application_commands.enums import ApplicationCommandType, \ + ApplicationCommandPermissionType, ApplicationCommandOptionType +from dislord.discord.interactions.receiving_and_responding.interaction import InteractionContextType +from dislord.discord.resources.application.enums import ApplicationIntegrationType +from dislord.discord.resources.channel.channel import ChannelType +from dislord.discord.reference import Snowflake, Missing, Locale +from dislord.types import ObjDict + + +class ApplicationCommandPermissions(ObjDict): + id: Snowflake + type: ApplicationCommandPermissionType + permission: bool + + +class GuildApplicationCommandPermissions(ObjDict): + id: Snowflake + application_id: Snowflake + guild_id: Snowflake + permissions: list[ApplicationCommandPermissions] + + +ApplicationCommandPermissionsObject = GuildApplicationCommandPermissions + + +class ApplicationCommandOptionChoice(ObjDict): + name: str + name_localizations: dict[Locale, str] | Missing | None + value: str | int | float + + +class ApplicationCommandOption(ObjDict): + type: ApplicationCommandOptionType + name: str + name_localizations: dict[Locale, str] | Missing | None + description: str + description_localizations: dict[Locale, str] | Missing | None + required: bool | Missing + choices: list[ApplicationCommandOptionChoice] | Missing + options: list['ApplicationCommandOption'] | Missing + channel_types: list[ChannelType] | Missing + min_value: int | float | Missing + max_values: int | float | Missing + min_length: int | Missing + max_length: int | Missing + autocomplete: bool | Missing + + +class ApplicationCommand(ObjDict): + id: Snowflake + type: ApplicationCommandType | Missing + application_id: Snowflake + guild_id: Snowflake | Missing + name: str + name_localizations: dict[Locale, str] | Missing| None + description: str + description_localizations: dict[Locale, str] | Missing | None + options: list[ApplicationCommandOption] | Missing + default_member_permissions: Permissions | None + dm_permission: bool | Missing # Deprecated + default_permission: bool | Missing | None = True + nsfw: bool | Missing = False + integration_types: list[ApplicationIntegrationType] | Missing = [ApplicationIntegrationType.GUILD_INSTALL] + contexts: list[InteractionContextType] | Missing | None + version: Snowflake \ No newline at end of file diff --git a/tests/cogs/base/__init__.py b/dislord/discord/interactions/components/__init__.py similarity index 100% rename from tests/cogs/base/__init__.py rename to dislord/discord/interactions/components/__init__.py diff --git a/dislord/discord/interactions/components/enums.py b/dislord/discord/interactions/components/enums.py new file mode 100644 index 00000000..18e2d520 --- /dev/null +++ b/dislord/discord/interactions/components/enums.py @@ -0,0 +1,25 @@ +from enum import Enum + + +class ComponentType(Enum): + ACTION_ROW = 1 + BUTTON = 2 + STRING_SELECT = 3 + TEXT_INPUT = 4 + USER_SELECT = 5 + ROLE_SELECT = 6 + MENTIONABLE_SELECT = 7 + CHANNEL_SELECT = 8 + + +class ButtonStyle(Enum): + PRIMARY = 1 + SECONDARY = 2 + SUCCESS = 3 + DANGER = 4 + LINK = 5 + + +class TextInputStyle(Enum): + SHORT = 1 + PARAGRAPH = 2 diff --git a/dislord/discord/interactions/components/models.py b/dislord/discord/interactions/components/models.py new file mode 100644 index 00000000..4fc6822a --- /dev/null +++ b/dislord/discord/interactions/components/models.py @@ -0,0 +1,60 @@ +from dislord.types import ObjDict +from dislord.discord.interactions.components.enums import ButtonStyle, TextInputStyle, ComponentType +from dislord.discord.resources.channel.channel import ChannelType +from dislord.discord.reference import Snowflake, Missing +from dislord.discord.resources.emoji.emoji import PartialEmoji + + +class TextInput(ObjDict): + type: ComponentType = ComponentType.TEXT_INPUT + custom_id: str + style: TextInputStyle + label: str + min_length: int | Missing + max_length: int | Missing + required: bool | Missing + value: str | Missing + placeholder: str | Missing + + +class SelectDefaultValue(ObjDict): + id: Snowflake + type: str + + +class SelectOption(ObjDict): + label: str + value: str + description: str | Missing + # emoji: PartialEmoji | Missing FIXME + default: bool | Missing + + +class SelectMenu(ObjDict): + type: ComponentType + custom_id: str + options: list[SelectOption] | Missing + channel_types: list[ChannelType] | Missing + placeholder: str | Missing + default_values: list[SelectDefaultValue] | Missing + min_values: int | Missing + max_values: int | Missing + disabled: bool | Missing + + +class Button(ObjDict): + type: ComponentType = ComponentType.BUTTON + style: ButtonStyle + label: str | None + emoji: PartialEmoji | Missing + custom_id: str | Missing + url: str | Missing + disabled: bool | Missing + + +class ActionRow(ObjDict): + type: ComponentType = ComponentType.ACTION_ROW + components: list[Button | SelectMenu | TextInput] + + +Component = ActionRow | Button | SelectMenu | TextInput diff --git a/tests/cogs/colour_role/__init__.py b/dislord/discord/interactions/receiving_and_responding/__init__.py similarity index 100% rename from tests/cogs/colour_role/__init__.py rename to dislord/discord/interactions/receiving_and_responding/__init__.py diff --git a/dislord/discord/interactions/receiving_and_responding/interaction.py b/dislord/discord/interactions/receiving_and_responding/interaction.py new file mode 100644 index 00000000..75d4cad2 --- /dev/null +++ b/dislord/discord/interactions/receiving_and_responding/interaction.py @@ -0,0 +1,66 @@ +from enum import Enum +from typing import Literal + +from dislord.types import ObjDict +from dislord.discord.interactions.receiving_and_responding.interaction_data import InteractionData, \ + ApplicationCommandData, MessageComponentData, ModalSubmitData +from dislord.discord.interactions.receiving_and_responding.message_interaction import InteractionType +from dislord.discord.resources.application.enums import ApplicationIntegrationType +from dislord.discord.resources.application.models import ApplicationIntegrationTypeConfiguration +from dislord.discord.resources.channel.channel import PartialChannel +from dislord.discord.resources.channel.message import Message +from dislord.discord.resources.guild.guild import Guild +from dislord.discord.resources.guild.guild_member import GuildMember +from dislord.discord.resources.user.user import User +from dislord.discord.reference import Snowflake, Missing, Locale + + +class InteractionContextType(Enum): + GUILD = 0 + BOT_DM = 1 + PRIVATE_CHANNEL = 2 + + +class __Interaction(ObjDict): + id: Snowflake + application_id: Snowflake + type: InteractionType + data: InteractionData | Missing + guild: Guild | Missing + guild_id: Snowflake | Missing + channel: PartialChannel | Missing + channel_id: Snowflake | Missing + member: GuildMember | Missing + user: User | Missing + token: str + version: int + message: Message | Missing + app_permissions: str + locale: Locale | Missing + guild_locale: Locale | Missing + # entitlements: list[Entitlement] FIXME + authorizing_integration_owners: dict[ApplicationIntegrationType, Snowflake] + context: InteractionContextType | Missing + + +class PingInteraction(__Interaction): + type: Literal[1] + data: Missing + + +class ApplicationCommandInteraction(__Interaction): + type: Literal[2, 4] + data: ApplicationCommandData + + +class MessageInteraction(__Interaction): + type: Literal[3] + data: MessageComponentData + + +class ModalSubmitInteraction(__Interaction): + type: Literal[5] + data: ModalSubmitData + + +Interaction = PingInteraction | ApplicationCommandInteraction | MessageInteraction | ModalSubmitInteraction diff --git a/dislord/discord/interactions/receiving_and_responding/interaction_data.py b/dislord/discord/interactions/receiving_and_responding/interaction_data.py new file mode 100644 index 00000000..41055a5d --- /dev/null +++ b/dislord/discord/interactions/receiving_and_responding/interaction_data.py @@ -0,0 +1,54 @@ +from typing import Self + +from dislord.types import ObjDict +from dislord.discord.interactions.application_commands.enums import ApplicationCommandOptionType +from dislord.discord.interactions.components.models import SelectOption, Component +from dislord.discord.topics.permissions.role import Role +from dislord.discord.resources.channel.attachment import Attachment +from dislord.discord.resources.channel.channel import PartialChannel +from dislord.discord.resources.channel.partial_message import PartialMessage +from dislord.discord.resources.guild.guild_member import PartialGuildMember +from dislord.discord.resources.user.user import User +from dislord.discord.reference import Snowflake, Missing + + +class ApplicationCommandInteractionDataOption(ObjDict): + name: str + type: ApplicationCommandOptionType + value: str | int | float | bool | Missing + options: list[Self] | Missing + focused: bool | Missing + + +class ModalSubmitData(ObjDict): + custom_id: str + components: list[Component] + + +class ResolvedData(ObjDict): + users: dict[Snowflake, User] | Missing + members: dict[Snowflake, PartialGuildMember] | Missing + roles: dict[Snowflake, Role] | Missing + channels: dict[Snowflake, PartialChannel] | Missing + messages: dict[Snowflake, PartialMessage] | Missing + attachments: dict[Snowflake, Attachment] | Missing + + +class MessageComponentData(ObjDict): + custom_id: str + component_type: int + values: list[SelectOption] | Missing + resolved: ResolvedData | Missing + + +class ApplicationCommandData(ObjDict): + id: Snowflake + name: str + type: int + resolved: ResolvedData | Missing + options: list[ApplicationCommandInteractionDataOption] | Missing + guild_id: Snowflake | Missing + target: Snowflake | Missing + + +InteractionData = ApplicationCommandData | MessageComponentData | ModalSubmitData diff --git a/dislord/discord/interactions/receiving_and_responding/interaction_response.py b/dislord/discord/interactions/receiving_and_responding/interaction_response.py new file mode 100644 index 00000000..dd3db255 --- /dev/null +++ b/dislord/discord/interactions/receiving_and_responding/interaction_response.py @@ -0,0 +1,61 @@ +from enum import Enum + +from dislord.types import ObjDict +from dislord.discord.interactions.application_commands.models import ApplicationCommandOptionChoice +from dislord.discord.interactions.components.models import Component +from dislord.discord.resources.channel.allowed_mentions import AllowedMentions +from dislord.discord.resources.channel.attachment import PartialAttachment +from dislord.discord.resources.channel.embed import Embed +from dislord.discord.resources.channel.message import MessageFlags +from dislord.discord.reference import Missing +from dislord.discord.resources.poll.poll import Poll + + +class ModalInteractionCallbackData(ObjDict): + custom_id: str + title: str + components: list[Component] + + +class AutocompleteInteractionCallbackData(ObjDict): + choices: list[ApplicationCommandOptionChoice] + + +class MessagesInteractionCallbackData(ObjDict): + tts: bool | Missing + content: str | Missing + embeds: list[Embed] | Missing + allowed_mentions: AllowedMentions | Missing + flags: MessageFlags + components: list[Component] | Missing + attachments: list[PartialAttachment] | Missing + poll: Poll | Missing + + +InteractionCallbackData = MessagesInteractionCallbackData | AutocompleteInteractionCallbackData | ModalInteractionCallbackData + + +class InteractionCallbackType(Enum): + PONG = 1 + CHANNEL_MESSAGE_WITH_SOURCE = 4 + DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE = 5 + DEFERRED_UPDATE_MESSAGE = 6 + UPDATE_MESSAGE = 7 # Only valid for component-based receiving_and_responding + APPLICATION_COMMAND_AUTOCOMPLETE_RESULT = 8 + MODAL = 9 # Not available for MODAL_SUBMIT and PING receiving_and_responding. + PREMIUM_REQUIRED = 10 # Not available for APPLICATION_COMMAND_AUTOCOMPLETE and PING receiving_and_responding. + + +class InteractionResponse(ObjDict): + type: InteractionCallbackType + data: InteractionCallbackData | Missing + + @staticmethod + def pong(): + return InteractionResponse(type=InteractionCallbackType.PONG) + + @staticmethod + def message(**kwargs): + cls = InteractionResponse(type=InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE, + data=MessagesInteractionCallbackData(**kwargs)) + return cls diff --git a/dislord/discord/interactions/receiving_and_responding/message_interaction.py b/dislord/discord/interactions/receiving_and_responding/message_interaction.py new file mode 100644 index 00000000..1b8db585 --- /dev/null +++ b/dislord/discord/interactions/receiving_and_responding/message_interaction.py @@ -0,0 +1,22 @@ +from enum import Enum + +from dislord.types import ObjDict +from dislord.discord.resources.guild.guild_member import PartialGuildMember +from dislord.discord.resources.user.user import User +from dislord.discord.reference import Missing, Snowflake + + +class InteractionType(Enum): + PING = 1 + APPLICATION_COMMAND = 2 + MESSAGE_COMPONENT = 3 + APPLICATION_COMMAND_AUTOCOMPLETE = 4 + MODAL_SUBMIT = 5 + + +class MessageInteraction(ObjDict): + id: Snowflake + type: InteractionType + name: str + user: User + member: PartialGuildMember | Missing diff --git a/dislord/discord/reference.py b/dislord/discord/reference.py new file mode 100644 index 00000000..741adb02 --- /dev/null +++ b/dislord/discord/reference.py @@ -0,0 +1,63 @@ +from enum import Enum + +from dislord.types import ObjDict + +DISCORD_API_VERSION = 10 +DISCORD_URL = f"https://discord.com/api/v{DISCORD_API_VERSION}" + + +class TokenType(Enum): + BOT = "Bot" + OAUTH2 = "Bearer" + + +class Authorization(ObjDict): + token_type: TokenType + token: str + + def to_authorization_header(self): + return {"Authorization": f"{self.token_type.value} {self.token}"} + + +Snowflake = str + +ISOTimestamp = type(str) # ISO8601 Timestamp + +Missing = type(None) + + +class Locale(Enum): + INDONESIAN = 'id' + DANISH = 'da' + GERMAN = 'de' + BRITISH_ENGLISH = 'en-GB' + AMERICAN_ENGLISH = 'en-US' + SPAIN_SPANISH = 'es-ES' + FRENCH = 'fr' + CROATIAN = 'hr' + ITALIAN = 'it' + LITHUANIAN = 'lt' + HUNGARIAN = 'hu' + DUTCH = 'nl' + NORWEGIAN = 'no' + POLISH = 'pl' + BRAZIL_PORTUGUESE = 'pt-BR' + ROMANIAN = 'ro' + FINNISH = 'fi' + SWEDISH = 'sv-SE' + VIETNAMESE = 'vi' + TURKISH = 'tr' + CZECH = 'cs' + GREEK = 'el' + BULGARIAN = 'bg' + RUSSIAN = 'ru' + UKRAINIAN = 'uk' + HINDI = 'hi' + THAI = 'th' + CHINESE = 'zh-CN' + JAPANESE = 'ja' + TAIWAN_CHINESE = 'zh-TW' + KOREAN = 'ko' + + def __str__(self) -> str: + return self.value diff --git a/tests/cogs/insights/__init__.py b/dislord/discord/resources/__init__.py similarity index 100% rename from tests/cogs/insights/__init__.py rename to dislord/discord/resources/__init__.py diff --git a/tests/cogs/intro_cog/__init__.py b/dislord/discord/resources/application/__init__.py similarity index 100% rename from tests/cogs/intro_cog/__init__.py rename to dislord/discord/resources/application/__init__.py diff --git a/dislord/discord/resources/application/enums.py b/dislord/discord/resources/application/enums.py new file mode 100644 index 00000000..920751dc --- /dev/null +++ b/dislord/discord/resources/application/enums.py @@ -0,0 +1,6 @@ +from enum import IntEnum + + +class ApplicationIntegrationType(IntEnum): + GUILD_INSTALL = 0 + USER_INSTALL = 1 diff --git a/dislord/discord/resources/application/flags.py b/dislord/discord/resources/application/flags.py new file mode 100644 index 00000000..59a2b1da --- /dev/null +++ b/dislord/discord/resources/application/flags.py @@ -0,0 +1,14 @@ +from enum import IntFlag + + +class ApplicationFlags(IntFlag): + APPLICATION_AUTO_MODERATION_RULE_CREATE_BADGE = 1 << 6 + GATEWAY_PRESENCE = 1 << 12 + GATEWAY_PRESENCE_LIMITED = 1 << 13 + GATEWAY_GUILD_MEMBERS = 1 << 14 + GATEWAY_GUILD_MEMBERS_LIMITED = 1 << 15 + VERIFICATION_PENDING_GUILD_LIMIT = 1 << 16 + EMBEDDED = 1 << 17 + GATEWAY_MESSAGE_CONTENT = 1 << 18 + GATEWAY_MESSAGE_CONTENT_LIMITED = 1 << 19 + APPLICATION_COMMAND_BADGE = 1 << 23 diff --git a/dislord/discord/resources/application/models.py b/dislord/discord/resources/application/models.py new file mode 100644 index 00000000..4a438b5e --- /dev/null +++ b/dislord/discord/resources/application/models.py @@ -0,0 +1,49 @@ +from dislord.discord.resources.guild.guild import PartialGuild +from dislord.discord.resources.user.user import User +from dislord.types import ObjDict +from dislord.discord.resources.application.enums import ApplicationIntegrationType +from dislord.discord.resources.application.flags import ApplicationFlags +from dislord.discord.reference import Snowflake, Missing + + +class InstallParams(ObjDict): + scopes: list[str] + permissions: str + + +class ApplicationIntegrationTypeConfiguration(ObjDict): + oauth2_install_params: InstallParams | Missing + + +class PartialApplication(ObjDict): + id: Snowflake + name: str + icon: str | None + description: str + bot_public: bool + bot_require_code_grant: bool + summary: str # depreciated v11 + verify_key: str + # team: Team | None FIXME + + +class Application(PartialApplication): + rpc_origins: list[str] | Missing + bot: User | Missing + terms_of_service_url: str | Missing + privacy_policy_url: str | Missing + owner: User | Missing + guild_id: Snowflake | Missing + guild: PartialGuild | Missing + primary_sku_id: Snowflake | Missing + slug: str | Missing + cover_image: str | Missing + flags: ApplicationFlags | Missing + approximate_guild_count: int | Missing + redirect_uris: list[str] | Missing + interactions_endpoint_url: str | Missing + role_connections_verification_url: str | Missing + tags: list[str] | Missing + install_params: InstallParams | Missing + integration_types_config: dict[ApplicationIntegrationType, ApplicationIntegrationTypeConfiguration] | Missing + custom_install_url: str | Missing diff --git a/tests/cogs/react_for_role/__init__.py b/dislord/discord/resources/application_role_connection_metadata/__init__.py similarity index 100% rename from tests/cogs/react_for_role/__init__.py rename to dislord/discord/resources/application_role_connection_metadata/__init__.py diff --git a/dislord/discord/resources/application_role_connection_metadata/enums.py b/dislord/discord/resources/application_role_connection_metadata/enums.py new file mode 100644 index 00000000..081fafe1 --- /dev/null +++ b/dislord/discord/resources/application_role_connection_metadata/enums.py @@ -0,0 +1,12 @@ +from enum import IntEnum + + +class ApplicationRoleConnectionMetadataType(IntEnum): + INTEGER_LESS_THAN_OR_EQUAL = 1 + INTEGER_GREATER_THAN_OR_EQUAL = 2 + INTEGER_EQUAL = 3 + INTEGER_NOT_EQUAL = 4 + DATETIME_LESS_THAN_OR_EQUAL = 5 + DATETIME_GREATER_THAN_OR_EQUAL = 6 + BOOLEAN_EQUAL = 7 + BOOLEAN_NOT_EQUAL = 8 \ No newline at end of file diff --git a/dislord/discord/resources/application_role_connection_metadata/models.py b/dislord/discord/resources/application_role_connection_metadata/models.py new file mode 100644 index 00000000..39b1ad0e --- /dev/null +++ b/dislord/discord/resources/application_role_connection_metadata/models.py @@ -0,0 +1,12 @@ +from dislord.types import ObjDict +from dislord.discord.resources.application_role_connection_metadata.enums import ApplicationRoleConnectionMetadataType +from dislord.discord.reference import Missing, Locale + + +class ApplicationRoleConnectionMetadata(ObjDict): + type: ApplicationRoleConnectionMetadataType + key: str + name: str + name_localizations: dict[Locale, str] | Missing | None + description: str + description_localizations: dict[Locale, str] | Missing | None \ No newline at end of file diff --git a/tests/cogs/text_filter/__init__.py b/dislord/discord/resources/audit_log/__init__.py similarity index 100% rename from tests/cogs/text_filter/__init__.py rename to dislord/discord/resources/audit_log/__init__.py diff --git a/dislord/discord/resources/audit_log/enums.py b/dislord/discord/resources/audit_log/enums.py new file mode 100644 index 00000000..0869b7ee --- /dev/null +++ b/dislord/discord/resources/audit_log/enums.py @@ -0,0 +1,69 @@ +from enum import IntEnum + + +class AuditLogEvent(IntEnum): + GUILD_UPDATE = 1 + CHANNEL_CREATE = 10 + CHANNEL_UPDATE = 11 + CHANNEL_DELETE = 12 + CHANNEL_OVERWRITE_CREATE = 13 + CHANNEL_OVERWRITE_UPDATE = 14 + CHANNEL_OVERWRITE_DELETE = 15 + MEMBER_KICK = 20 + MEMBER_PRUNE = 21 + MEMBER_BAN_ADD = 22 + MEMBER_BAN_REMOVE = 23 + MEMBER_UPDATE = 24 + MEMBER_ROLE_UPDATE = 25 + MEMBER_MOVE = 26 + MEMBER_DISCONNECT = 27 + BOT_ADD = 28 + ROLE_CREATE = 30 + ROLE_UPDATE = 31 + ROLE_DELETE = 32 + INVITE_CREATE = 40 + INVITE_UPDATE = 41 + INVITE_DELETE = 42 + WEBHOOK_CREATE = 50 + WEBHOOK_UPDATE = 51 + WEBHOOK_DELETE = 52 + EMOJI_CREATE = 60 + EMOJI_UPDATE = 61 + EMOJI_DELETE = 62 + MESSAGE_DELETE = 72 + MESSAGE_BULK_DELETE = 73 + MESSAGE_PIN = 74 + MESSAGE_UNPIN = 75 + INTEGRATION_CREATE = 80 + INTEGRATION_UPDATE = 81 + INTEGRATION_DELETE = 82 + STAGE_INSTANCE_CREATE = 83 + STAGE_INSTANCE_UPDATE = 84 + STAGE_INSTANCE_DELETE = 85 + STICKER_CREATE = 90 + STICKER_UPDATE = 91 + STICKER_DELETE = 92 + GUILD_SCHEDULED_EVENT_CREATE = 100 + GUILD_SCHEDULED_EVENT_UPDATE = 101 + GUILD_SCHEDULED_EVENT_DELETE = 102 + THREAD_CREATE = 110 + THREAD_UPDATE = 111 + THREAD_DELETE = 112 + APPLICATION_COMMAND_PERMISSION_UPDATE = 121 + AUTO_MODERATION_RULE_CREATE = 140 + AUTO_MODERATION_RULE_UPDATE = 141 + AUTO_MODERATION_RULE_DELETE = 142 + AUTO_MODERATION_BLOCK_MESSAGE = 143 + AUTO_MODERATION_FLAG_TO_CHANNEL = 144 + AUTO_MODERATION_USER_COMMUNICATION_DISABLED = 145 + CREATOR_MONETIZATION_REQUEST_CREATED = 150 + CREATOR_MONETIZATION_TERMS_ACCEPTED = 151 + ONBOARDING_PROMPT_CREATE = 163 + ONBOARDING_PROMPT_UPDATE = 164 + ONBOARDING_PROMPT_DELETE = 165 + ONBOARDING_CREATE = 166 + ONBOARDING_UPDATE = 167 + HOME_SETTINGS_CREATE = 190 + HOME_SETTINGS_UPDATE = 191 + + diff --git a/dislord/discord/resources/audit_log/models.py b/dislord/discord/resources/audit_log/models.py new file mode 100644 index 00000000..6debcebb --- /dev/null +++ b/dislord/discord/resources/audit_log/models.py @@ -0,0 +1,48 @@ +from dislord.types import ObjDict +from dislord.discord.interactions.application_commands.models import ApplicationCommand +from dislord.discord.resources.audit_log.enums import AuditLogEvent +from dislord.discord.resources.auto_moderation.models import AutoModerationRule +from dislord.discord.resources.channel.channel import Channel +from dislord.discord.reference import Snowflake, Missing + + +class AuditLogChange(ObjDict): + new_value: str | int | float | Missing + old_value: str | int | float | Missing + key: str + + +class OptionalAuditEntryInfo(ObjDict): + application_id: Snowflake + auto_moderation_rule_name: str + auto_moderation_rule_trigger_type: str + channel_id: Snowflake + count: str + delete_member_days: str + id: Snowflake + members_removed: str + message_id: Snowflake + role_name: str + type: str + integration_type: str + + +class AuditLogEntry(ObjDict): + target_id: str | None + changes: list[AuditLogChange] | Missing + user_id: Snowflake | None + id: Snowflake + action_type: AuditLogEvent + options: OptionalAuditEntryInfo | Missing + reason: str | Missing + + +class AuditLog(ObjDict): + application_commands: list[ApplicationCommand] + audit_log_entries: list[AuditLogEntry] + auto_moderation_rules: list[AutoModerationRule] + # guild_scheduled_events: list[GuildScheduledEvent] FIXME + # integrations: list[PartialIntegration] FIXME + threads: list[Channel] + # users: list[User] FIXME + # webhooks: list[Webhook] FIXME diff --git a/tests/cogs/twitch_alert/__init__.py b/dislord/discord/resources/auto_moderation/__init__.py similarity index 100% rename from tests/cogs/twitch_alert/__init__.py rename to dislord/discord/resources/auto_moderation/__init__.py diff --git a/dislord/discord/resources/auto_moderation/enums.py b/dislord/discord/resources/auto_moderation/enums.py new file mode 100644 index 00000000..3deee192 --- /dev/null +++ b/dislord/discord/resources/auto_moderation/enums.py @@ -0,0 +1,24 @@ +from enum import IntEnum + + +class TriggerType(IntEnum): + KEYWORD = 1 + SPAM = 3 + KEYWORD_PRESET = 4 + MENTION_SPAM = 5 + + +class KeywordPresetType(IntEnum): + PROFANITY = 1 + SEXUAL_CONTENT = 2 + SLURS = 3 + + +class EventType(IntEnum): + MESSAGE_SEND = 1 + + +class ActionType(IntEnum): + BLOCK_MESSAGE = 1 + SEND_ALERT_MESSAGE = 2 + TIMEOUT = 3 diff --git a/dislord/discord/resources/auto_moderation/models.py b/dislord/discord/resources/auto_moderation/models.py new file mode 100644 index 00000000..05131338 --- /dev/null +++ b/dislord/discord/resources/auto_moderation/models.py @@ -0,0 +1,36 @@ +from dislord.types import ObjDict +from dislord.discord.resources.auto_moderation.enums import TriggerType, EventType, KeywordPresetType, ActionType +from dislord.discord.reference import Snowflake, Missing + +class ActionMetadata(ObjDict): + channel_id: Snowflake + duration_seconds: int + custom_message: str | Missing + + +class AutoModerationAction(ObjDict): + type: ActionType + metadata: ActionMetadata | Missing + + +class TriggerMetadata(ObjDict): + keyword_filter: list[str] + regex_patterns: list[str] + presets: list[KeywordPresetType] + allow_list: list[str] + mention_total_limit: int + mention_raid_protection_enabled: bool + + +class AutoModerationRule(ObjDict): + id: Snowflake + guild_id: Snowflake + name: str + creator_id: Snowflake + event_type: EventType + trigger_type: TriggerType + trigger_metadata: TriggerMetadata + actions: list[AutoModerationAction] + enabled: bool + exempt_roles: list[Snowflake] + exempt_channels: list[Snowflake] \ No newline at end of file diff --git a/tests/cogs/verification/__init__.py b/dislord/discord/resources/channel/__init__.py similarity index 100% rename from tests/cogs/verification/__init__.py rename to dislord/discord/resources/channel/__init__.py diff --git a/dislord/discord/resources/channel/allowed_mentions.py b/dislord/discord/resources/channel/allowed_mentions.py new file mode 100644 index 00000000..20e2351c --- /dev/null +++ b/dislord/discord/resources/channel/allowed_mentions.py @@ -0,0 +1,17 @@ +from enum import StrEnum + +from dislord.types import ObjDict +from dislord.discord.reference import Snowflake + + +class AllowedMentionType(StrEnum): + ROLES = "roles" + USERS = "users" + EVERYONE = "everyone" + + +class AllowedMentions(ObjDict): + parse: list[AllowedMentionType] + role: list[Snowflake] + users: list[Snowflake] + replied_user: bool diff --git a/dislord/discord/resources/channel/attachment.py b/dislord/discord/resources/channel/attachment.py new file mode 100644 index 00000000..b99ee464 --- /dev/null +++ b/dislord/discord/resources/channel/attachment.py @@ -0,0 +1,27 @@ +from enum import IntFlag + +from dislord.types import ObjDict +from dislord.discord.reference import Snowflake, Missing + + +class AttachmentFlag(IntFlag): + IS_REMIX = 1 << 2 + + +class PartialAttachment(ObjDict): + id: Snowflake + filename: str + description: str | Missing + + +class Attachment(PartialAttachment): + content_type: str | Missing + size: int + url: str + proxy_url: str + height: int | Missing | None + width: int | Missing | None + ephemeral: bool | Missing + duration_secs: float | Missing + waveform: str | Missing + flags: AttachmentFlag diff --git a/dislord/discord/resources/channel/channel.py b/dislord/discord/resources/channel/channel.py new file mode 100644 index 00000000..db9e8fc9 --- /dev/null +++ b/dislord/discord/resources/channel/channel.py @@ -0,0 +1,89 @@ +from enum import IntFlag, IntEnum + +from dislord.types import ObjDict +from dislord.discord.resources.channel.default_reaction import DefaultReaction +from dislord.discord.resources.channel.forum_tag import ForumTag +from dislord.discord.resources.channel.overwrite import Overwrite +from dislord.discord.resources.channel.thread_member import ThreadMember +from dislord.discord.resources.channel.thread_metadata import ThreadMetadata +from dislord.discord.resources.user.user import User +from dislord.discord.reference import Missing, Snowflake, ISOTimestamp + + +class ForumLayoutType(IntEnum): + NOT_SET = 0 + LIST_VIEW = 1 + GALLERY_VIEW = 2 + + +class SortOrderType(IntEnum): + LATEST_ACTIVITY = 0 + CREATION_DATE = 1 + + +class ChannelFlags(IntFlag): + PINNED = 1 << 1 + REQUIRE_TAG = 1 << 4 + HIDE_MEDIA_DOWNLOAD_OPTIONS = 1 << 15 + + +class VideoQualityMode(IntEnum): + AUTO = 1 + FULL = 2 + + +class ChannelType(IntEnum): + GUILD_TEXT = 0 + DM = 1 + GUILD_VOICE = 2 + GROUP_DM = 3 + GUILD_CATEGORY = 4 + GUILD_ANNOUNCEMENT = 5 + ANNOUNCEMENT_THREAD = 10 + PUBLIC_THREAD = 11 + PRIVATE_THREAD = 12 + GUILD_STAGE_VOICE = 13 + GUILD_DIRECTORY = 14 + GUILD_FORUM = 15 + GUILD_MEDIA = 16 + + +class PartialChannel(ObjDict): + id: Snowflake + type: ChannelType + name: str | Missing | None + permissions: str | Missing + thread_metadata: ThreadMetadata | Missing + parent_id: Snowflake | Missing | None + + +class Channel(PartialChannel): + guild_id: Snowflake | Missing + position: int | Missing + permission_overwrites: list[Overwrite] | Missing + topic: str | Missing | None + nsfw: bool | Missing + last_message_id: Snowflake | Missing | None + bitrate: int | Missing + user_limit: int | Missing + rate_limit_per_user: int | Missing + recipients: list[User] | Missing + icon: str | Missing | None + owner_id: Snowflake | Missing + application_id: Snowflake | Missing + managed: bool | Missing + last_pin_timestamp: ISOTimestamp | Missing | None + rtc_region: str | Missing | None + video_quality_mode: int | Missing + message_count: int | Missing + member_count: int | Missing + member: ThreadMember | Missing + default_auto_archive_duration: int | Missing + flags: ChannelFlags | Missing + total_message_sent: int | Missing + available_tags: list[ForumTag] | Missing + applied_tags: list[Snowflake] | Missing + default_reaction_emoji: DefaultReaction | Missing | None + default_thread_rate_limit_per_user: int | Missing + default_sort_order: int | Missing | None + default_forum_layout: int | Missing diff --git a/dislord/discord/resources/channel/channel_mention.py b/dislord/discord/resources/channel/channel_mention.py new file mode 100644 index 00000000..efed9445 --- /dev/null +++ b/dislord/discord/resources/channel/channel_mention.py @@ -0,0 +1,10 @@ +from dislord.types import ObjDict +from dislord.discord.resources.channel.channel import ChannelType +from dislord.discord.reference import Snowflake + + +class ChannelMention(ObjDict): + id: Snowflake + guild_id: Snowflake + type: ChannelType + name: str diff --git a/dislord/discord/resources/channel/default_reaction.py b/dislord/discord/resources/channel/default_reaction.py new file mode 100644 index 00000000..6f883880 --- /dev/null +++ b/dislord/discord/resources/channel/default_reaction.py @@ -0,0 +1,8 @@ +from dislord.types import ObjDict +from dislord.discord.reference import Snowflake +from dislord.types import ObjDict + + +class DefaultReaction(ObjDict): + emoji_id: Snowflake | None + emoji_name: str | None diff --git a/dislord/discord/resources/channel/embed.py b/dislord/discord/resources/channel/embed.py new file mode 100644 index 00000000..fdf641f4 --- /dev/null +++ b/dislord/discord/resources/channel/embed.py @@ -0,0 +1,63 @@ +from dislord.types import ObjDict +from dislord.discord.reference import Missing, ISOTimestamp + + +class EmbedField(ObjDict): + name: str + value: str + inline: bool | Missing + + +class EmbedFooter(ObjDict): + text: str + icon_url: str | Missing + proxy_icon_url: str | Missing + + +class EmbedAuthor(ObjDict): + name: str + url: str | Missing + icon_url: str | Missing + proxy_icon_url: str | Missing + + +class EmbedProvider(ObjDict): + name: str | Missing + url: str | Missing + + +class EmbedImage(ObjDict): + url: str + proxy_url: str | Missing + height: int | Missing + width: int | Missing + + +class EmbedVideo(ObjDict): + url: str | Missing + proxy_url: str | Missing + height: int | Missing + width: int | Missing + + +class EmbedThumbnail(ObjDict): + url: str + proxy_url: str | Missing + height: int | Missing + width: int | Missing + + +class Embed(ObjDict): + title: str | Missing + type: str | Missing + description: str | Missing + url: str | Missing + timestamp: ISOTimestamp | Missing + color: int | Missing + footer: EmbedFooter | Missing + image: EmbedImage | Missing + thumbnail: EmbedThumbnail | Missing + video: EmbedVideo | Missing + provider: EmbedProvider | Missing + author: EmbedAuthor | Missing + fields: list[EmbedField] | Missing diff --git a/dislord/discord/resources/channel/enums.py b/dislord/discord/resources/channel/enums.py new file mode 100644 index 00000000..f49acb15 --- /dev/null +++ b/dislord/discord/resources/channel/enums.py @@ -0,0 +1,5 @@ +from enum import IntEnum, StrEnum + + + + diff --git a/dislord/discord/resources/channel/followed_channel.py b/dislord/discord/resources/channel/followed_channel.py new file mode 100644 index 00000000..f3650a98 --- /dev/null +++ b/dislord/discord/resources/channel/followed_channel.py @@ -0,0 +1,7 @@ +from dislord.types import ObjDict +from dislord.discord.reference import Snowflake + + +class FollowedChannel(ObjDict): + channel_id: Snowflake + webhook_id: Snowflake diff --git a/dislord/discord/resources/channel/forum_tag.py b/dislord/discord/resources/channel/forum_tag.py new file mode 100644 index 00000000..33550752 --- /dev/null +++ b/dislord/discord/resources/channel/forum_tag.py @@ -0,0 +1,10 @@ +from dislord.types import ObjDict +from dislord.discord.reference import Snowflake + + +class ForumTag(ObjDict): + id: Snowflake + name: str + moderated: bool + emoji_id: Snowflake | None + emoji_name: str | None diff --git a/dislord/discord/resources/channel/message.py b/dislord/discord/resources/channel/message.py new file mode 100644 index 00000000..f03fdf47 --- /dev/null +++ b/dislord/discord/resources/channel/message.py @@ -0,0 +1,66 @@ +from enum import IntFlag, IntEnum +from typing import Self + +from dislord.types import ObjDict +from dislord.discord.interactions.components.models import Component +from dislord.discord.interactions.receiving_and_responding.interaction_data import ResolvedData +from dislord.discord.interactions.receiving_and_responding.message_interaction import MessageInteraction +from dislord.discord.resources.application.models import PartialApplication +from dislord.discord.resources.channel.channel import Channel +from dislord.discord.resources.channel.channel_mention import ChannelMention +from dislord.discord.resources.channel.message_interaction_metadata import MessageInteractionMetadata +from dislord.discord.resources.channel.message_reference import MessageReference +from dislord.discord.resources.channel.partial_message import PartialMessage +from dislord.discord.resources.channel.role_subscription_data import RoleSubscriptionData +from dislord.discord.resources.poll.poll import Poll +from dislord.discord.resources.sticker.sticker import Sticker +from dislord.discord.resources.sticker.sticker_item import StickerItem +from dislord.discord.reference import Missing, Snowflake + + +class MessageFlags(IntFlag): + CROSSPOSTED = 1 << 0 + IS_CROSSPOST = 1 << 1 + SUPPRESS_EMBEDS = 1 << 2 + SOURCE_MESSAGE_DELETED = 1 << 3 + URGENT = 1 << 4 + HAS_THREAD = 1 << 5 + EPHEMERAL = 1 << 6 + LOADING = 1 << 7 + FAILED_TO_MENTION_SOME_ROLES_IN_THREAD = 1 << 8 + SUPPRESS_NOTIFICATIONS = 1 << 12 + IS_VOICE_MESSAGE = 1 << 13 + + +class MessageActivityType(IntEnum): + JOIN = 1 + SPECTATE = 2 + LISTEN = 3 + JOIN_REQUEST = 5 + + +class MessageActivity(ObjDict): + type: MessageActivityType + party_id: str | Missing + + +class Message(PartialMessage): + mention_channels: list[ChannelMention] | Missing + nonce: int | str | Missing + webhook_id: Snowflake | Missing + activity: MessageActivity | Missing + application: PartialApplication | Missing + application_id: Snowflake | Missing + message_reference: MessageReference | Missing + flags: MessageFlags | Missing + referenced_message: Self | Missing | None + interaction_metadata: MessageInteractionMetadata | Missing + interaction: MessageInteraction | Missing + thread: Channel | Missing + components: list[Component] | Missing + sticker_items: list[StickerItem] | Missing + stickers: list[Sticker] | Missing + position: int | Missing + role_subscription_data = RoleSubscriptionData | Missing + resolved: ResolvedData | Missing + poll: Poll | Missing diff --git a/dislord/discord/resources/channel/message_call.py b/dislord/discord/resources/channel/message_call.py new file mode 100644 index 00000000..fed37ddb --- /dev/null +++ b/dislord/discord/resources/channel/message_call.py @@ -0,0 +1,7 @@ +from dislord.types import ObjDict +from dislord.discord.reference import Snowflake, ISOTimestamp, Missing + + +class MessageCall(ObjDict): + participants: list[Snowflake] + ended_timestamp: ISOTimestamp | Missing | None diff --git a/dislord/discord/resources/channel/message_interaction_metadata.py b/dislord/discord/resources/channel/message_interaction_metadata.py new file mode 100644 index 00000000..7113ddfb --- /dev/null +++ b/dislord/discord/resources/channel/message_interaction_metadata.py @@ -0,0 +1,17 @@ +from typing import Self + +from dislord.types import ObjDict +from dislord.discord.interactions.receiving_and_responding.message_interaction import InteractionType +from dislord.discord.resources.application.enums import ApplicationIntegrationType +from dislord.discord.resources.user.user import User +from dislord.discord.reference import Snowflake, Missing + + +class MessageInteractionMetadata(ObjDict): + id: Snowflake + type: InteractionType + user: User + authorizing_integration_owners: dict[ApplicationIntegrationType, Snowflake] + original_response_message_id: Snowflake | Missing + interacted_message_id: Snowflake | Missing + triggering_interaction_metadata: Self | Missing diff --git a/dislord/discord/resources/channel/message_reference.py b/dislord/discord/resources/channel/message_reference.py new file mode 100644 index 00000000..cf19468b --- /dev/null +++ b/dislord/discord/resources/channel/message_reference.py @@ -0,0 +1,9 @@ +from dislord.types import ObjDict +from dislord.discord.reference import Snowflake, Missing + + +class MessageReference(ObjDict): + message_id: Snowflake | Missing + channel_id: Snowflake | Missing + guild_id: Snowflake | Missing + fail_if_not_exists: bool | Missing diff --git a/dislord/discord/resources/channel/overwrite.py b/dislord/discord/resources/channel/overwrite.py new file mode 100644 index 00000000..90908e79 --- /dev/null +++ b/dislord/discord/resources/channel/overwrite.py @@ -0,0 +1,16 @@ +from enum import IntEnum + +from dislord.types import ObjDict +from dislord.discord.reference import Snowflake + + +class OverwriteType(IntEnum): + ROLE = 0 + MEMBER = 1 + + +class Overwrite(ObjDict): + id: Snowflake + type: OverwriteType + allow: str + deny: str diff --git a/dislord/discord/resources/channel/partial_message.py b/dislord/discord/resources/channel/partial_message.py new file mode 100644 index 00000000..8546f1e5 --- /dev/null +++ b/dislord/discord/resources/channel/partial_message.py @@ -0,0 +1,61 @@ +from enum import IntEnum + +from dislord.types import ObjDict +from dislord.discord.topics.permissions.role import Role +from dislord.discord.resources.channel.attachment import Attachment +from dislord.discord.resources.channel.embed import Embed +from dislord.discord.resources.channel.reaction import Reaction +from dislord.discord.resources.user.user import User +from dislord.discord.reference import Snowflake, ISOTimestamp + + +class MessageType(IntEnum): + DEFAULT = 0 + RECIPIENT_ADD = 1 + RECIPIENT_REMOVE = 2 + CALL = 3 + CHANNEL_NAME_CHANGE = 4 + CHANNEL_ICON_CHANGE = 5 + CHANNEL_PINNED_MESSAGE = 6 + USER_JOIN = 7 + GUILD_BOOST = 8 + GUILD_BOOST_TIER_1 = 9 + GUILD_BOOST_TIER_2 = 10 + GUILD_BOOST_TIER_3 = 11 + CHANNEL_FOLLOW_ADD = 12 + GUILD_DISCOVERY_DISQUALIFIED = 14 + GUILD_DISCOVERY_REQUALIFIED = 15 + GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING = 16 + GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING = 17 + THREAD_CREATED = 18 + REPLY = 19 + CHAT_INPUT_COMMAND = 20 + THREAD_STARTER_MESSAGE = 21 + GUILD_INVITE_REMINDER = 22 + CONTEXT_MENU_COMMAND = 23 + AUTO_MODERATION_ACTION = 24 + ROLE_SUBSCRIPTION_PURCHASE = 25 + INTERACTION_PREMIUM_UPSELL = 26 + STAGE_START = 27 + STAGE_END = 28 + STAGE_SPEAKER = 29 + STAGE_TOPIC = 31 + GUILD_APPLICATION_PREMIUM_SUBSCRIPTION = 32 + + +class PartialMessage(ObjDict): + id: Snowflake + channel_id: Snowflake + author: User + content: str + timestamp: ISOTimestamp + edited_timestamp: ISOTimestamp | None + tts: bool + mention_everyone: bool + mentions: list[User] + mention_roles: list[Role] + attachments: list[Attachment] + embeds: list[Embed] + reactions: list[Reaction] + pinned: bool + type: MessageType diff --git a/dislord/discord/resources/channel/reaction.py b/dislord/discord/resources/channel/reaction.py new file mode 100644 index 00000000..2236ee36 --- /dev/null +++ b/dislord/discord/resources/channel/reaction.py @@ -0,0 +1,13 @@ +from dislord.types import ObjDict +from dislord.model.base import BaseModel, HexColor +from dislord.discord.resources.channel.reaction_count_details import ReactionCountDetails +from dislord.discord.resources.emoji.emoji import PartialEmoji + + +class Reaction(ObjDict): + count: int + count_details: ReactionCountDetails + me: bool + me_burst: bool + emoji: PartialEmoji + bust_colors: list[HexColor] diff --git a/dislord/discord/resources/channel/reaction_count_details.py b/dislord/discord/resources/channel/reaction_count_details.py new file mode 100644 index 00000000..71810833 --- /dev/null +++ b/dislord/discord/resources/channel/reaction_count_details.py @@ -0,0 +1,6 @@ +from dislord.types import ObjDict + + +class ReactionCountDetails(ObjDict): + burst: int + normal: int diff --git a/dislord/discord/resources/channel/role_subscription_data.py b/dislord/discord/resources/channel/role_subscription_data.py new file mode 100644 index 00000000..8d5ab6d4 --- /dev/null +++ b/dislord/discord/resources/channel/role_subscription_data.py @@ -0,0 +1,10 @@ +from dislord.types import ObjDict +from dislord.discord.reference import Snowflake + + +class RoleSubscriptionData(ObjDict): + role_subscription_listing_id = Snowflake + tier_name: str + total_months_subscribed = int + is_renewal = bool + diff --git a/dislord/discord/resources/channel/thread_member.py b/dislord/discord/resources/channel/thread_member.py new file mode 100644 index 00000000..1ce32969 --- /dev/null +++ b/dislord/discord/resources/channel/thread_member.py @@ -0,0 +1,13 @@ +from enum import IntFlag + +from dislord.types import ObjDict +from dislord.discord.resources.guild.guild_member import GuildMember +from dislord.discord.reference import Snowflake, Missing, ISOTimestamp + + +class ThreadMember(ObjDict): + id: Snowflake | Missing + user_id: Snowflake | Missing + join_timestamp: ISOTimestamp + flags: IntFlag # Any user-thread settings, currently only used for notifications + member: GuildMember | Missing diff --git a/dislord/discord/resources/channel/thread_metadata.py b/dislord/discord/resources/channel/thread_metadata.py new file mode 100644 index 00000000..19b64414 --- /dev/null +++ b/dislord/discord/resources/channel/thread_metadata.py @@ -0,0 +1,11 @@ +from dislord.types import ObjDict +from dislord.discord.reference import ISOTimestamp, Missing + + +class ThreadMetadata(ObjDict): + archived: bool + auto_archive_duration: int + archive_timestamp: ISOTimestamp + locked: bool + invitable: bool | Missing + create_timestamp: ISOTimestamp | Missing | None diff --git a/tests/cogs/voting/__init__.py b/dislord/discord/resources/emoji/__init__.py similarity index 100% rename from tests/cogs/voting/__init__.py rename to dislord/discord/resources/emoji/__init__.py diff --git a/dislord/discord/resources/emoji/emoji.py b/dislord/discord/resources/emoji/emoji.py new file mode 100644 index 00000000..53ef9c8d --- /dev/null +++ b/dislord/discord/resources/emoji/emoji.py @@ -0,0 +1,17 @@ +from dislord.types import ObjDict +from dislord.discord.resources.user.user import User +from dislord.discord.reference import Snowflake, Missing + + +class PartialEmoji(ObjDict): + id: Snowflake | None + name: str | None + animated: bool | Missing + + +class Emoji(PartialEmoji): + roles: list[Snowflake] | Missing + user: User + require_colons: bool | Missing + managed: bool | Missing + available: bool | Missing diff --git a/dislord/discord/resources/guild/__init__.py b/dislord/discord/resources/guild/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dislord/discord/resources/guild/ban.py b/dislord/discord/resources/guild/ban.py new file mode 100644 index 00000000..0c190348 --- /dev/null +++ b/dislord/discord/resources/guild/ban.py @@ -0,0 +1,8 @@ + +from dislord.types import ObjDict +from dislord.discord.resources.user.user import User + + +class Ban(ObjDict): + reason: str | None + user: User diff --git a/dislord/discord/resources/guild/guild.py b/dislord/discord/resources/guild/guild.py new file mode 100644 index 00000000..c693b1cc --- /dev/null +++ b/dislord/discord/resources/guild/guild.py @@ -0,0 +1,141 @@ +from enum import IntEnum, IntFlag, Flag + +from dislord.types import ObjDict +from dislord.discord.topics.permissions.role import Role +from dislord.discord.resources.emoji.emoji import Emoji +from dislord.discord.resources.guild.welcome_screen import WelcomeScreen +from dislord.discord.resources.sticker.sticker import Sticker +from dislord.discord.reference import Missing, Snowflake, Locale + + +class MutableGuildFeatures(Flag): + COMMUNITY = "COMMUNITY" + DISCOVERABLE = "DISCOVERABLE" + INVITES_DISABLED = "INVITES_DISABLED" + RAID_ALERTS_DISABLED = "RAID_ALERTS_DISABLED" + + +class GuildFeatures(Flag): + ANIMATED_BANNER = "ANIMATED_BANNER" + ANIMATED_ICON = "ANIMATED_ICON" + APPLICATION_COMMAND_PERMISSIONS_V2 = "APPLICATION_COMMAND_PERMISSIONS_V2" + AUTO_MODERATION = "AUTO_MODERATION" + BANNER = "BANNER" + COMMUNITY = "COMMUNITY" + CREATOR_MONETIZABLE_PROVISIONAL = "CREATOR_MONETIZABLE_PROVISIONAL" + CREATOR_STORE_PAGE = "CREATOR_STORE_PAGE" + DEVELOPER_SUPPORT_SERVER = "DEVELOPER_SUPPORT_SERVER" + DISCOVERABLE = "DISCOVERABLE" + FEATURABLE = "FEATURABLE" + INVITES_DISABLED = "INVITES_DISABLED" + INVITE_SPLASH = "INVITE_SPLASH" + MEMBER_VERIFICATION_GATE_ENABLED = "MEMBER_VERIFICATION_GATE_ENABLED" + MORE_STICKERS = "MORE_STICKERS" + NEWS = "NEWS" + PARTNERED = "PARTNERED" + PREVIEW_ENABLED = "PREVIEW_ENABLED" + RAID_ALERTS_DISABLED = "RAID_ALERTS_DISABLED" + ROLE_ICONS = "ROLE_ICONS" + ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE = "ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE" + ROLE_SUBSCRIPTIONS_ENABLED = "ROLE_SUBSCRIPTIONS_ENABLED" + TICKETED_EVENTS_ENABLED = "TICKETED_EVENTS_ENABLED" + VANITY_URL = "VANITY_URL" + VERIFIED = "VERIFIED" + VIP_REGIONS = "VIP_REGIONS" + WELCOME_SCREEN_ENABLED = "WELCOME_SCREEN_ENABLED" + + +class SystemChannelFlags(IntFlag): + SUPPRESS_JOIN_NOTIFICATIONS = 1 << 0 + SUPPRESS_PREMIUM_SUBSCRIPTIONS = 1 << 1 + SUPPRESS_GUILD_REMINDER_NOTIFICATIONS = 1 << 2 + SUPPRESS_JOIN_NOTIFICATION_REPLIES = 1 << 3 + SUPPRESS_ROLE_SUBSCRIPTION_PURCHASE_NOTIFICATIONS = 1 << 4 + SUPPRESS_ROLE_SUBSCRIPTION_PURCHASE_NOTIFICATION_REPLIES = 1 << 5 + + +class PremiumTier(IntEnum): + NONE = 0 + TIER_1 = 1 + TIER_2 = 2 + TIER_3 = 3 + + +class GuildNsfwLevel(IntEnum): + DEFAULT = 0 + EXPLICIT = 1 + SAFE = 2 + AGE_RESTRICTED = 3 + + +class VerificationLevel(IntEnum): + NONE = 0 + LOW = 1 + MEDIUM = 2 + HIGH = 3 + VERY_HIGH = 4 + + +class MfaLevel(IntEnum): + NONE = 0 + ELEVATED = 1 + + +class ExplicitContentFilterLevel(IntEnum): + DISABLED = 0 + MEMBERS_WITHOUT_ROLES = 1 + ALL_MEMBERS = 2 + + +class DefaultMessageNotificationLevel(IntEnum): + ALL_MESSAGES = 0 + ONLY_MENTIONS = 1 + + +class PartialGuild(ObjDict): + id: Snowflake + name: str + description: str | None + region: str | Missing + afk_channel_id: Snowflake | None + system_channel_id: Snowflake | None + icon_hash: str | Missing | None + + +class Guild(PartialGuild): + icon: str | None + splash: str | None + discovery_splash: str | None + owner: bool | Missing + owner_id: Snowflake + permissions: str | Missing + afk_timeout: int + widget_enabled: bool | Missing + widget_channel_id: Snowflake | Missing | None + verification_level: VerificationLevel + default_message_notifications: DefaultMessageNotificationLevel + explicit_content_filter: ExplicitContentFilterLevel + roles: list[Role] + emojis: list[Emoji] + features: list[GuildFeatures] + mfa_level: MfaLevel + application_id: Snowflake | None + system_channel_flags: SystemChannelFlags + rules_channel_id: Snowflake | None + max_presences: int | Missing | None + max_members: int | Missing + vanity_url_code: str | None + banner: str | None + premium_tier: PremiumTier + premium_subscription_count: int | Missing + preferred_locale: Locale + public_updates_channel_id: Snowflake | None + max_video_channel_users: int | Missing + max_stage_video_channel_users: int | Missing + approximate_member_count: int | Missing + approximate_presence_count: int | Missing + welcome_screen: WelcomeScreen | Missing + nsfw_level: GuildNsfwLevel + stickers: list[Sticker] | Missing + premium_progress_bar_enabled: bool + safety_alerts_channel_id: Snowflake | None diff --git a/dislord/discord/resources/guild/guild_member.py b/dislord/discord/resources/guild/guild_member.py new file mode 100644 index 00000000..356d8fa0 --- /dev/null +++ b/dislord/discord/resources/guild/guild_member.py @@ -0,0 +1,32 @@ +from enum import IntFlag + +from dislord.types import ObjDict +from dislord.discord.resources.user.avatar_decoration_data import AvatarDecorationData +from dislord.discord.resources.user.user import User +from dislord.discord.reference import Missing, Snowflake, ISOTimestamp + + +class GuildMemberFlags(IntFlag): + DID_REJOIN = 1 << 0 + COMPLETED_ONBOARDING = 1 << 1 + BYPASSES_VERIFICATION = 1 << 2 + STARTED_ONBOARDING = 1 << 3 + + +class PartialGuildMember(ObjDict): + nick: str | Missing | None + avatar: str | Missing | None + roles: list[Snowflake] + joined_at: ISOTimestamp + premium_since: ISOTimestamp | Missing | None + flags: GuildMemberFlags + pending: bool | Missing + permissions: str | Missing + communication_disabled_until: ISOTimestamp | Missing | None + avatar_decoration_data: AvatarDecorationData | Missing | None + + +class GuildMember(PartialGuildMember): + user: User | Missing + deaf: bool + mute: bool diff --git a/dislord/discord/resources/guild/guild_onboarding.py b/dislord/discord/resources/guild/guild_onboarding.py new file mode 100644 index 00000000..1a996acc --- /dev/null +++ b/dislord/discord/resources/guild/guild_onboarding.py @@ -0,0 +1,45 @@ +from enum import IntFlag + +from dislord.types import ObjDict +from dislord.discord.resources.emoji.emoji import Emoji +from dislord.discord.reference import Snowflake, Missing + + +class PromptType(IntFlag): + MULTIPLE_CHOICE = 0 + DROPDOWN = 1 + + +class OnboardingMode(IntFlag): + ONBOARDING_DEFAULT = 0 + ONBOARDING_ADVANCED = 1 + + +class PromptOption(ObjDict): + id: Snowflake + channel_ids: list[Snowflake] + role_ids: list[Snowflake] + emoji: Emoji | Missing + emoji_id: Snowflake | Missing + emoji_name: str | Missing + emoji_animated: bool | Missing + title: str + description: str + + +class OnboardingPrompt(Snowflake): + id: Snowflake + type: PromptType + options: list[PromptOption] + title: str + single_select: bool + required: bool + in_onboarding: bool + + +class GuildOnboarding(ObjDict): + guild_id: Snowflake + prompts: list[OnboardingPrompt] + default_channel_ids: list[Snowflake] + enabled: bool + mode: OnboardingMode diff --git a/dislord/discord/resources/guild/guild_preview.py b/dislord/discord/resources/guild/guild_preview.py new file mode 100644 index 00000000..80c2dc95 --- /dev/null +++ b/dislord/discord/resources/guild/guild_preview.py @@ -0,0 +1,19 @@ +from dislord.types import ObjDict +from dislord.discord.resources.emoji.emoji import Emoji +from dislord.discord.resources.guild.guild import GuildFeatures +from dislord.discord.resources.sticker.sticker import Sticker +from dislord.discord.reference import Snowflake + + +class GuildPreview(ObjDict): + id: Snowflake + name: str + icon: str | None + splash: str | None + discovery_splash: str | None + emojis: list[Emoji] + features: list[GuildFeatures] + approximate_member_count: int + approximate_presence_count: int + description: str | None + stickers: list[Sticker] diff --git a/dislord/discord/resources/guild/guild_widget.py b/dislord/discord/resources/guild/guild_widget.py new file mode 100644 index 00000000..3a989a27 --- /dev/null +++ b/dislord/discord/resources/guild/guild_widget.py @@ -0,0 +1,13 @@ +from dislord.types import ObjDict +from dislord.discord.resources.channel.channel import PartialChannel +from dislord.discord.resources.user.user import PartialUser +from dislord.discord.reference import Snowflake + + +class GuildWidget(ObjDict): + id: Snowflake + name: str + instant_invite: str | None + channels: list[PartialChannel] + members: list[PartialUser] + presence_count: int diff --git a/dislord/discord/resources/guild/guild_widget_settings.py b/dislord/discord/resources/guild/guild_widget_settings.py new file mode 100644 index 00000000..3073f2bb --- /dev/null +++ b/dislord/discord/resources/guild/guild_widget_settings.py @@ -0,0 +1,7 @@ +from dislord.types import ObjDict +from dislord.discord.reference import Snowflake + + +class GuildWidgetSettings(ObjDict): + enabled: bool + channel_id: Snowflake | None diff --git a/dislord/discord/resources/guild/integration.py b/dislord/discord/resources/guild/integration.py new file mode 100644 index 00000000..7eda7c1f --- /dev/null +++ b/dislord/discord/resources/guild/integration.py @@ -0,0 +1,31 @@ +from enum import IntEnum + +from dislord.types import ObjDict +from dislord.discord.resources.guild.integration_account import IntegrationAccount +from dislord.discord.resources.guild.integration_application import IntegrationApplication +from dislord.discord.resources.user.user import User +from dislord.discord.reference import Snowflake, Missing, ISOTimestamp + + +class IntegrationExpireBehavior(IntEnum): + REMOVE_ROLE = 0 + KICK = 1 + + +class Integration(ObjDict): + id: Snowflake + name: str + type: str + enabled: bool + syncing: bool | Missing + role_id: Snowflake | Missing + enable_emoticons: bool | Missing + expire_behavior: IntegrationExpireBehavior | Missing + expire_grace_period: int | Missing + user: User | Missing + account: IntegrationAccount + synced_at: ISOTimestamp | Missing + subscriber_count: int | Missing + revoked: bool | Missing + application: IntegrationApplication | Missing + # scopes: list[Oauth2Scopes] | Missing FIXME diff --git a/dislord/discord/resources/guild/integration_account.py b/dislord/discord/resources/guild/integration_account.py new file mode 100644 index 00000000..9eecac7d --- /dev/null +++ b/dislord/discord/resources/guild/integration_account.py @@ -0,0 +1,6 @@ +from dislord.types import ObjDict + + +class IntegrationAccount(ObjDict): + id: str + name: str diff --git a/dislord/discord/resources/guild/integration_application.py b/dislord/discord/resources/guild/integration_application.py new file mode 100644 index 00000000..c906c2be --- /dev/null +++ b/dislord/discord/resources/guild/integration_application.py @@ -0,0 +1,11 @@ +from dislord.types import ObjDict +from dislord.discord.resources.user.user import User +from dislord.discord.reference import Snowflake, Missing + + +class IntegrationApplication(ObjDict): + id: Snowflake + name: str + icon: str | None + description: str + bot: User | Missing diff --git a/dislord/discord/resources/guild/welcome_screen.py b/dislord/discord/resources/guild/welcome_screen.py new file mode 100644 index 00000000..c05b0e91 --- /dev/null +++ b/dislord/discord/resources/guild/welcome_screen.py @@ -0,0 +1,14 @@ +from dislord.types import ObjDict +from dislord.discord.reference import Snowflake + + +class WelcomeScreenChannel(ObjDict): + channel_id: Snowflake + description: str + emoji_id: Snowflake | None + emoji_name: str | None + + +class WelcomeScreen(ObjDict): + description: str | None + welcome_channel: list[WelcomeScreenChannel] diff --git a/dislord/discord/resources/guild_scheduled_event/__init__.py b/dislord/discord/resources/guild_scheduled_event/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dislord/discord/resources/guild_scheduled_event/guild_scheduled_event.py b/dislord/discord/resources/guild_scheduled_event/guild_scheduled_event.py new file mode 100644 index 00000000..22e7bf49 --- /dev/null +++ b/dislord/discord/resources/guild_scheduled_event/guild_scheduled_event.py @@ -0,0 +1,45 @@ +from enum import IntEnum + +from dislord.types import ObjDict +from dislord.discord.resources.user.user import User +from dislord.discord.reference import Snowflake, Missing, ISOTimestamp + + +class GuildScheduledEventEntityMetadata(ObjDict): + location: str | Missing + + +class GuildScheduledEventStatus(IntEnum): + SCHEDULED = 1 + ACTIVE = 2 + COMPLETED = 3 + CANCELED = 4 + + +class GuildScheduledEventEntityType(IntEnum): + STAGE_INSTANCE = 1 + VOICE = 2 + EXTERNAL = 3 + + +class GuildScheduledEventPrivacyLevel(IntEnum): + GUILD_ONLY = 2 + + +class GuildScheduledEvent(ObjDict): + id: Snowflake + guild_id: Snowflake + channel_id: Snowflake | None + creator_id: Snowflake | Missing | None + name: str + description: str | Missing | None + scheduled_start_time: ISOTimestamp + scheduled_ent_time: ISOTimestamp | None + privacy_level: GuildScheduledEventPrivacyLevel + status: GuildScheduledEventStatus + entity_type: GuildScheduledEventEntityType + entity_id: Snowflake | None + entity_metadata: GuildScheduledEventEntityMetadata | None + creator: User | Missing + user_count: int | Missing + image: str | Missing | None diff --git a/dislord/discord/resources/guild_scheduled_event/guild_scheduled_event_user.py b/dislord/discord/resources/guild_scheduled_event/guild_scheduled_event_user.py new file mode 100644 index 00000000..e576f96b --- /dev/null +++ b/dislord/discord/resources/guild_scheduled_event/guild_scheduled_event_user.py @@ -0,0 +1,10 @@ +from dislord.types import ObjDict +from dislord.discord.resources.guild.guild_member import GuildMember +from dislord.discord.resources.user.user import User +from dislord.discord.reference import Snowflake, Missing + + +class GuildScheduledEventUser(ObjDict): + guild_scheduled_event_id: Snowflake + user: User + member: GuildMember | Missing diff --git a/dislord/discord/resources/guild_template/__init__.py b/dislord/discord/resources/guild_template/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dislord/discord/resources/guild_template/guild_template.py b/dislord/discord/resources/guild_template/guild_template.py new file mode 100644 index 00000000..209b2f1d --- /dev/null +++ b/dislord/discord/resources/guild_template/guild_template.py @@ -0,0 +1,18 @@ +from dislord.types import ObjDict +from dislord.discord.resources.guild.guild import PartialGuild +from dislord.discord.resources.user.user import User +from dislord.discord.reference import Snowflake, ISOTimestamp + + +class GuildTemplate(ObjDict): + code: str + name: str + description: str | None + usage_count: int + creator_id: Snowflake + creator: User + created_at: ISOTimestamp + updated_at: ISOTimestamp + source_guild_id: Snowflake + serialized_source_guild: PartialGuild + is_dirty: bool | None diff --git a/dislord/discord/resources/invite/__init__.py b/dislord/discord/resources/invite/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dislord/discord/resources/invite/invite.py b/dislord/discord/resources/invite/invite.py new file mode 100644 index 00000000..1d4599ca --- /dev/null +++ b/dislord/discord/resources/invite/invite.py @@ -0,0 +1,37 @@ +from enum import IntEnum + +from dislord.types import ObjDict +from dislord.discord.resources.application.models import PartialApplication +from dislord.discord.resources.channel.channel import PartialChannel +from dislord.discord.resources.guild.guild import PartialGuild +from dislord.discord.resources.guild_scheduled_event.guild_scheduled_event import GuildScheduledEvent +from dislord.discord.resources.invite.invite_stage_instance import InviteStageInstance +from dislord.discord.resources.user.user import User +from dislord.discord.reference import Missing, ISOTimestamp + + +class InviteTargetType(IntEnum): + STREAM = 1 + EMBEDDED_APPLICATION = 2 + + +class InviteType(IntEnum): + GUILD = 0 + GROUP_DM = 1 + FRIEND = 2 + + +class Invite(ObjDict): + type: InviteType + code: str + guild: PartialGuild | Missing + channel: PartialChannel | None + inviter: User | Missing + target_type: InviteTargetType | Missing + target_user: User | Missing + target_application: PartialApplication | Missing + approximate_presence_count = int | Missing + approximate_member_count: int | Missing + expires_at: ISOTimestamp | Missing | None + stage_instance: InviteStageInstance | Missing + guild_scheduled_event: GuildScheduledEvent | Missing diff --git a/dislord/discord/resources/invite/invite_metadata.py b/dislord/discord/resources/invite/invite_metadata.py new file mode 100644 index 00000000..331ce529 --- /dev/null +++ b/dislord/discord/resources/invite/invite_metadata.py @@ -0,0 +1,10 @@ +from dislord.types import ObjDict +from dislord.discord.reference import ISOTimestamp + + +class InviteMetadata(ObjDict): + uses: int + max_uses: int + max_age: int + temporary: bool + created_at: ISOTimestamp diff --git a/dislord/discord/resources/invite/invite_stage_instance.py b/dislord/discord/resources/invite/invite_stage_instance.py new file mode 100644 index 00000000..c73215be --- /dev/null +++ b/dislord/discord/resources/invite/invite_stage_instance.py @@ -0,0 +1,9 @@ +from dislord.types import ObjDict +from dislord.discord.resources.guild.guild_member import PartialGuildMember + + +class InviteStageInstance(ObjDict): + members: list[PartialGuildMember] + participant_count: int + speaker_count: int + topic: str diff --git a/dislord/discord/resources/poll/__init__.py b/dislord/discord/resources/poll/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dislord/discord/resources/poll/layout_type.py b/dislord/discord/resources/poll/layout_type.py new file mode 100644 index 00000000..779e8eee --- /dev/null +++ b/dislord/discord/resources/poll/layout_type.py @@ -0,0 +1,5 @@ +from enum import IntEnum + + +class LayoutType(IntEnum): + DEFAULT = 1 diff --git a/dislord/discord/resources/poll/poll.py b/dislord/discord/resources/poll/poll.py new file mode 100644 index 00000000..cf1ada38 --- /dev/null +++ b/dislord/discord/resources/poll/poll.py @@ -0,0 +1,15 @@ +from dislord.types import ObjDict +from dislord.discord.resources.poll.layout_type import LayoutType +from dislord.discord.resources.poll.poll_answer import PollAnswer +from dislord.discord.resources.poll.poll_media import PollMedia +from dislord.discord.resources.poll.poll_results import PollResults +from dislord.discord.reference import ISOTimestamp, Missing + + +class Poll(ObjDict): + question: PollMedia + answers: list[PollAnswer] + expiry: ISOTimestamp | None + allow_multiset: bool + layout_type: LayoutType + results: PollResults | Missing \ No newline at end of file diff --git a/dislord/discord/resources/poll/poll_answer.py b/dislord/discord/resources/poll/poll_answer.py new file mode 100644 index 00000000..9ccde1c6 --- /dev/null +++ b/dislord/discord/resources/poll/poll_answer.py @@ -0,0 +1,7 @@ +from dislord.types import ObjDict +from dislord.discord.resources.poll.poll_media import PollMedia + + +class PollAnswer(ObjDict): + answer_id: int + poll_media: PollMedia diff --git a/dislord/discord/resources/poll/poll_create_request.py b/dislord/discord/resources/poll/poll_create_request.py new file mode 100644 index 00000000..a06279c1 --- /dev/null +++ b/dislord/discord/resources/poll/poll_create_request.py @@ -0,0 +1,13 @@ +from dislord.discord.reference import Missing +from dislord.discord.resources.poll.layout_type import LayoutType +from dislord.discord.resources.poll.poll_answer import PollAnswer +from dislord.discord.resources.poll.poll_media import PollMedia +from dislord.types import ObjDict + + +class PollCreateRequest(ObjDict): + question: PollMedia + answers: list[PollAnswer] + duration: int + allow_multiset: bool + layout_type: LayoutType | Missing diff --git a/dislord/discord/resources/poll/poll_media.py b/dislord/discord/resources/poll/poll_media.py new file mode 100644 index 00000000..0cad151b --- /dev/null +++ b/dislord/discord/resources/poll/poll_media.py @@ -0,0 +1,8 @@ +from dislord.types import ObjDict +from dislord.discord.resources.emoji.emoji import PartialEmoji +from dislord.discord.reference import Missing + + +class PollMedia(ObjDict): + text: str | Missing + emoji: PartialEmoji | Missing diff --git a/dislord/discord/resources/poll/poll_results.py b/dislord/discord/resources/poll/poll_results.py new file mode 100644 index 00000000..374ead77 --- /dev/null +++ b/dislord/discord/resources/poll/poll_results.py @@ -0,0 +1,12 @@ +from dislord.types import ObjDict + + +class PollAnswerCount(ObjDict): + id: int + count: int + me_voted: bool + + +class PollResults(ObjDict): + is_finalized: bool + answer_counts: list[PollAnswerCount] diff --git a/dislord/discord/resources/stage_instance/__init__.py b/dislord/discord/resources/stage_instance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dislord/discord/resources/stage_instance/stage_instance.py b/dislord/discord/resources/stage_instance/stage_instance.py new file mode 100644 index 00000000..df6c657c --- /dev/null +++ b/dislord/discord/resources/stage_instance/stage_instance.py @@ -0,0 +1,17 @@ +from enum import IntEnum + +from dislord.types import ObjDict +from dislord.discord.reference import Snowflake + + +class PrivacyLevel(IntEnum): + PUBLIC = 1 + GUILD_ONLY = 2 + + +class StageInstance(ObjDict): + id: Snowflake + guild_id: Snowflake + channel_id: Snowflake + topic: str + privacy_level: PrivacyLevel diff --git a/dislord/discord/resources/sticker/__init__.py b/dislord/discord/resources/sticker/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dislord/discord/resources/sticker/sticker.py b/dislord/discord/resources/sticker/sticker.py new file mode 100644 index 00000000..2db95a76 --- /dev/null +++ b/dislord/discord/resources/sticker/sticker.py @@ -0,0 +1,32 @@ +from enum import IntEnum + +from dislord.types import ObjDict +from dislord.discord.resources.user.user import User +from dislord.discord.reference import Snowflake, Missing + + +class StickerFormatType(IntEnum): + PNG = 1 + APNG = 2 + LOTTIE = 3 + GIF = 4 + + +class StickerType(IntEnum): + STANDARD = 1 + GUILD = 2 + + +class Sticker(ObjDict): + id: Snowflake + pack_id: Snowflake | Missing + name: str + description: str | None + tags: str + asset: str | Missing # Deprecated + type: StickerType + format_type: StickerFormatType + available: bool | Missing + guild_id: Snowflake | Missing + user: User | Missing + sort_value: int | Missing diff --git a/dislord/discord/resources/sticker/sticker_item.py b/dislord/discord/resources/sticker/sticker_item.py new file mode 100644 index 00000000..03a5cf32 --- /dev/null +++ b/dislord/discord/resources/sticker/sticker_item.py @@ -0,0 +1,9 @@ +from dislord.types import ObjDict +from dislord.discord.resources.sticker.sticker import StickerFormatType +from dislord.discord.reference import Snowflake + + +class StickerItem(ObjDict): + id: Snowflake + name: str + format_type: StickerFormatType diff --git a/dislord/discord/resources/sticker/sticker_pack.py b/dislord/discord/resources/sticker/sticker_pack.py new file mode 100644 index 00000000..179f502f --- /dev/null +++ b/dislord/discord/resources/sticker/sticker_pack.py @@ -0,0 +1,13 @@ +from dislord.types import ObjDict +from dislord.discord.resources.sticker.sticker import Sticker +from dislord.discord.reference import Snowflake, Missing + + +class StickerPack(ObjDict): + id: Snowflake + stickers: list[Sticker] + name: str + sku_id: Snowflake + cover_sticker_id: Snowflake | Missing + description: str + banner_asset_id: Snowflake | Missing diff --git a/dislord/discord/resources/user/__init__.py b/dislord/discord/resources/user/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dislord/discord/resources/user/application_role_connection.py b/dislord/discord/resources/user/application_role_connection.py new file mode 100644 index 00000000..96d0efef --- /dev/null +++ b/dislord/discord/resources/user/application_role_connection.py @@ -0,0 +1,8 @@ +from dislord.types import ObjDict +from dislord.discord.resources.application_role_connection_metadata.models import ApplicationRoleConnectionMetadata + + +class ApplicationRoleConnection(ObjDict): + platform_name: str | None + platform_username: str | None + metadata: dict[ApplicationRoleConnectionMetadata, str] diff --git a/dislord/discord/resources/user/avatar_decoration_data.py b/dislord/discord/resources/user/avatar_decoration_data.py new file mode 100644 index 00000000..11c65cdd --- /dev/null +++ b/dislord/discord/resources/user/avatar_decoration_data.py @@ -0,0 +1,7 @@ +from dislord.types import ObjDict +from dislord.discord.reference import Snowflake + + +class AvatarDecorationData(ObjDict): + asset: str + sku_id: Snowflake diff --git a/dislord/discord/resources/user/connection.py b/dislord/discord/resources/user/connection.py new file mode 100644 index 00000000..308c4305 --- /dev/null +++ b/dislord/discord/resources/user/connection.py @@ -0,0 +1,23 @@ +from enum import IntEnum + +from dislord.types import ObjDict +from dislord.discord.resources.guild.integration import Integration +from dislord.discord.reference import Missing + + +class VisibilityType(IntEnum): + NONE = 0 + EVERYONE = 1 + + +class Connection(ObjDict): + id: str + name: str + type: str + revoked: bool | Missing + integrations: list[Integration] | Missing + verified: bool + friend_sync: bool + show_activity: bool + two_way_link: bool + visibility: VisibilityType diff --git a/dislord/discord/resources/user/user.py b/dislord/discord/resources/user/user.py new file mode 100644 index 00000000..897ec589 --- /dev/null +++ b/dislord/discord/resources/user/user.py @@ -0,0 +1,52 @@ +from enum import IntEnum, IntFlag + +from dislord.types import ObjDict +from dislord.discord.reference import Snowflake, Missing, Locale + + +class PremiumType(IntEnum): + NONE = 0 + NITRO_CLASSIC = 1 + NITRO = 2 + NITRO_BASIC = 3 + + +class UserFlags(IntFlag): + STAFF = 1 << 0 + PARTNER = 1 << 1 + HYPESQUAD = 1 << 2 + BUG_HUNTER_LEVEL_1 = 1 << 3 + HYPESQUAD_ONLINE_HOUSE_1 = 1 << 6 + HYPESQUAD_ONLINE_HOUSE_2 = 1 << 7 + HYPESQUAD_ONLINE_HOUSE_3 = 1 << 8 + PREMIUM_EARLY_SUPPORTER = 1 << 9 + TEAM_PSEUDO_USER = 1 << 10 + BUG_HUNTER_LEVEL_2 = 1 << 14 + VERIFIED_BOT = 1 << 16 + VERIFIED_DEVELOPER = 1 << 17 + CERTIFIED_MODERATOR = 1 << 18 + BOT_HTTP_INTERACTIONS = 1 << 19 + ACTIVE_DEVELOPER = 1 << 22 + + +class PartialUser(ObjDict): + id: Snowflake + username: str + discriminator: str + avatar: str | None + + +class User(PartialUser): + global_name: str | None + bot: bool | Missing + system: bool | Missing + mfa_enabled: bool | Missing + banner: str | Missing | None + accent_color: int | Missing | None + locale: Locale | Missing + verified: bool | Missing + email: str | Missing | None + flags: UserFlags | Missing + premium_type: PremiumType | Missing + public_flags: UserFlags | Missing + avatar_decoration: str | Missing | None diff --git a/dislord/discord/resources/voice/__init__.py b/dislord/discord/resources/voice/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dislord/discord/resources/voice/voice_region.py b/dislord/discord/resources/voice/voice_region.py new file mode 100644 index 00000000..c8007216 --- /dev/null +++ b/dislord/discord/resources/voice/voice_region.py @@ -0,0 +1,9 @@ +from dislord.types import ObjDict + + +class VoiceRegion(ObjDict): + id: str + name: str + optimal: bool + deprecated: bool + custom: bool diff --git a/dislord/discord/resources/voice/voice_state.py b/dislord/discord/resources/voice/voice_state.py new file mode 100644 index 00000000..2fb8a409 --- /dev/null +++ b/dislord/discord/resources/voice/voice_state.py @@ -0,0 +1,19 @@ +from dislord.types import ObjDict +from dislord.discord.resources.guild.guild_member import GuildMember +from dislord.discord.reference import Snowflake, Missing, ISOTimestamp + + +class VoiceState(ObjDict): + guild_id: Snowflake | Missing + channel_id: Snowflake | None + user_id: Snowflake + member: GuildMember | Missing + session_id: str + deaf: bool + mute: bool + self_deaf: bool + self_mute: bool + self_stream: bool | Missing + self_video: bool + suppress: bool + request_to_speak_timestamp: ISOTimestamp | None diff --git a/dislord/discord/resources/webhook/__init__.py b/dislord/discord/resources/webhook/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dislord/discord/resources/webhook/webhook.py b/dislord/discord/resources/webhook/webhook.py new file mode 100644 index 00000000..7134dfce --- /dev/null +++ b/dislord/discord/resources/webhook/webhook.py @@ -0,0 +1,27 @@ +from enum import IntEnum + +from dislord.types import ObjDict +from dislord.discord.resources.channel.channel import PartialChannel +from dislord.discord.resources.guild.guild import PartialGuild +from dislord.discord.resources.user.user import User +from dislord.discord.reference import Snowflake, Missing + + +class WebhookType(IntEnum): + INCOMING = 1 + CHANNEL_FOLLOWER = 2 + APPLICATION = 3 + + +class Webhook(ObjDict): + id: Snowflake + type: WebhookType + guild_id: Snowflake | Missing | None + channel_id: Snowflake | None + user: User | None + avatar: str | None + token: str | Missing + application_id: Snowflake | None + source_guild: PartialGuild | Missing + source_channel: PartialChannel | Missing + url: str | Missing diff --git a/dislord/discord/topics/__init__.py b/dislord/discord/topics/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dislord/discord/topics/permissions/__init__.py b/dislord/discord/topics/permissions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dislord/discord/topics/permissions/permissions.py b/dislord/discord/topics/permissions/permissions.py new file mode 100644 index 00000000..709a1630 --- /dev/null +++ b/dislord/discord/topics/permissions/permissions.py @@ -0,0 +1,52 @@ +from enum import IntFlag + + +class Permission(IntFlag): + CREATE_INSTANT_INVITE = 1 << 0 + KICK_MEMBERS = 1 << 1 + BAN_MEMBERS = 1 << 2 + ADMINISTRATOR = 1 << 3 + MANAGE_CHANNELS = 1 << 4 + MANAGE_GUILD = 1 << 5 + ADD_REACTIONS = 1 << 6 + VIEW_AUDIT_LOG = 1 << 7 + PRIORITY_SPEAKER = 1 << 8 + STREAM = 1 << 9 + VIEW_CHANNEL = 1 << 10 + SEND_MESSAGES = 1 << 11 + SEND_TTS_MESSAGES = 1 << 12 + MANAGE_MESSAGES = 1 << 13 + EMBED_LINKS = 1 << 14 + ATTACH_FILES = 1 << 15 + READ_MESSAGE_HISTORY = 1 << 16 + MENTION_EVERYONE = 1 << 17 + USE_EXTERNAL_EMOJIS = 1 << 18 + VIEW_GUILD_INSIGHTS = 1 << 19 + CONNECT = 1 << 20 + SPEAK = 1 << 21 + MUTE_MEMBERS = 1 << 22 + DEAFEN_MEMBERS = 1 << 23 + MOVE_MEMBERS = 1 << 24 + USE_VAD = 1 << 25 + CHANGE_NICKNAME = 1 << 26 + MANAGE_NICKNAMES = 1 << 27 + MANAGE_ROLES = 1 << 28 + MANAGE_WEBHOOKS = 1 << 29 + MANAGE_GUILD_EXPRESSIONS = 1 << 30 + USE_APPLICATION_COMMANDS = 1 << 31 + REQUEST_TO_SPEAK = 1 << 32 + MANAGE_EVENTS = 1 << 33 + MANAGE_THREADS = 1 << 34 + CREATE_PUBLIC_THREADS = 1 << 35 + CREATE_PRIVATE_THREADS = 1 << 36 + USE_EXTERNAL_STICKERS = 1 << 37 + SEND_MESSAGES_IN_THREADS = 1 << 38 + USE_EMBEDDED_ACTIVITIES = 1 << 39 + MODERATE_MEMBERS = 1 << 40 + VIEW_CREATOR_MONETIZATION_ANALYTICS = 1 << 41 + USE_SOUNDBOARD = 1 << 42 + CREATE_GUILD_EXPRESSIONS = 1 << 43 + CREATE_EVENTS = 1 << 44 + USE_EXTERNAL_SOUNDS = 1 << 45 + SEND_VOICE_MESSAGES = 1 << 46 + SEND_POLLS = 1 << 49 diff --git a/dislord/discord/topics/permissions/role.py b/dislord/discord/topics/permissions/role.py new file mode 100644 index 00000000..920431ae --- /dev/null +++ b/dislord/discord/topics/permissions/role.py @@ -0,0 +1,32 @@ +from enum import IntFlag + +from dislord.types import ObjDict +from dislord.discord.reference import Snowflake, Missing + + +class RoleTags(ObjDict): + bot_id: Snowflake | Missing + integration_id: Snowflake | Missing + premium_subscriber: Missing | None + subscription_listing_id: Snowflake | Missing + available_for_purchase: Missing | None + guild_connections: Missing | None + + +class RoleFlags(IntFlag): + IN_PROMPT = 1 << 0 + + +class Role(ObjDict): + id: Snowflake + name: str + color: int + hoist: bool + icon: str | Missing | None + unicode_emoji: str | Missing | None + position: int + permissions: str + managed: bool + mentionable: bool + tags: RoleTags | Missing + flags: RoleFlags diff --git a/dislord/discord/topics/teams/__init__.py b/dislord/discord/topics/teams/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dislord/discord/topics/teams/data_models.py b/dislord/discord/topics/teams/data_models.py new file mode 100644 index 00000000..0982f868 --- /dev/null +++ b/dislord/discord/topics/teams/data_models.py @@ -0,0 +1,26 @@ +from enum import IntEnum + +from dislord.types import ObjDict +from dislord.discord.reference import Snowflake +from dislord.discord.resources.user.user import PartialUser +from dislord.discord.topics.teams.team_member_roles import TeamMemberRoleType + + +class MembershipState(IntEnum): + INVITED = 1 + ACCEPTED = 2 + + +class TeamMember(ObjDict): + membership_state: int + team_id: Snowflake + user: PartialUser + role: TeamMemberRoleType + + +class Team(ObjDict): + icon: str | None + id: Snowflake + members: list[TeamMember] + name: str + owner_user_id: Snowflake diff --git a/dislord/discord/topics/teams/team_member_roles.py b/dislord/discord/topics/teams/team_member_roles.py new file mode 100644 index 00000000..31614794 --- /dev/null +++ b/dislord/discord/topics/teams/team_member_roles.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class TeamMemberRoleType(Enum): + OWNER = None + ADMIN = "admin" + DEVELOPER = "developer" + READ_ONLY = "read_only" diff --git a/dislord/dpyadapter/__init__.py b/dislord/dpyadapter/__init__.py new file mode 100644 index 00000000..d887f292 --- /dev/null +++ b/dislord/dpyadapter/__init__.py @@ -0,0 +1 @@ +from . import handlers diff --git a/dislord/dpyadapter/handlers.py b/dislord/dpyadapter/handlers.py new file mode 100644 index 00000000..2a5d02e4 --- /dev/null +++ b/dislord/dpyadapter/handlers.py @@ -0,0 +1,15 @@ +import discord + +from dislord.discord.interactions.receiving_and_responding.interaction_response import InteractionResponse, \ + InteractionCallbackType +from dislord.discord.resources.channel.message import MessageFlags + + +async def handle_response(response: InteractionResponse, dpy_interaction: discord.Interaction): + match response.type: + case InteractionCallbackType.PONG: + await dpy_interaction.response.pong() + case InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE: + await dpy_interaction.response.send_message(ephemeral=( + response.data.get("flags") is not None and MessageFlags.EPHEMERAL in response.data.get("flags")), + **response.data) diff --git a/dislord/error.py b/dislord/error.py new file mode 100644 index 00000000..ec164482 --- /dev/null +++ b/dislord/error.py @@ -0,0 +1,6 @@ +class StateConfigurationException(Exception): + MISSING_PUBLIC_TOKEN = "Public token has not been configured. Please use dislord.configure(...)" + + +class DiscordApiException(Exception): + UNKNOWN_INTERACTION_TYPE = "Unknown interaction type %s" diff --git a/dislord/group.py b/dislord/group.py new file mode 100644 index 00000000..dd8586e6 --- /dev/null +++ b/dislord/group.py @@ -0,0 +1,47 @@ +from typing import Callable + +from dislord.discord.interactions.application_commands.enums import ApplicationCommandOptionType +from dislord.discord.interactions.application_commands.models import ApplicationCommandOption +from dislord.discord.interactions.receiving_and_responding.interaction import Interaction +from dislord.discord.reference import Snowflake + + +class CommandGroup: + name: str + description: str + dm_permission: bool + nsfw: bool + guild_id: Snowflake + parent: 'CommandGroup' = None + commands: dict[str, ApplicationCommandOption] = {} + command_callbacks: dict[str, Callable] = {} + + def __init__(self, name: str = None, description: str = None, + dm_permission: bool = True, nsfw: bool = False, guild_id: Snowflake = None, + parent: 'CommandGroup' = None): + self.name = name + self.description = description + self.parent = parent + self.dm_permission = dm_permission + self.nsfw = nsfw + self.guild_id = guild_id + + def add_command(self, command: ApplicationCommandOption, callback: Callable): + self.commands[command.name] = command + self.command_callbacks[command.name] = callback + + def command(self, *, name, description, options: list[ApplicationCommandOption] = None): + def decorator(func): + self.add_command(ApplicationCommandOption(name=name, description=description, + type=ApplicationCommandOptionType.SUB_COMMAND, + options=options), func) + return func + + return decorator + + def callback(self, interaction: Interaction, **kwargs): + command_name = interaction.data.options[0].name + kwargs = {} + for option in interaction.data.options[0].options: + kwargs[option.name] = option.value + return self.command_callbacks[command_name](interaction, **kwargs) diff --git a/dislord/model/__init__.py b/dislord/model/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dislord/model/api.py b/dislord/model/api.py new file mode 100644 index 00000000..c01bf14f --- /dev/null +++ b/dislord/model/api.py @@ -0,0 +1,30 @@ +import json +from http.client import OK, UNAUTHORIZED + +from dislord.model.base import EnhancedJSONEncoder + + +class HttpResponse: + status_code: int + body: [dict, str] + headers: dict + + def __init__(self, body, *, headers=None): + self.body = body + self.headers = headers + + def as_serverless_response(self): + return {"statusCode": int(self.status_code), + "body": json.dumps(self.body, cls=EnhancedJSONEncoder), + "headers": self.headers} + + def as_server_response(self): + return self.body, int(self.status_code) + + +class HttpOk(HttpResponse): + status_code = OK + + +class HttpUnauthorized(HttpResponse): + status_code = UNAUTHORIZED diff --git a/dislord/model/application.py b/dislord/model/application.py new file mode 100644 index 00000000..761e70b3 --- /dev/null +++ b/dislord/model/application.py @@ -0,0 +1,13 @@ +from dislord.discord.resources.application.models import Application as ApplicationPayload +from dislord.model.base import BaseModel + + +class Application(BaseModel): + payload: ApplicationPayload + + @staticmethod + def from_payload(payload: ApplicationPayload) -> 'Application': + return Application( + payload=payload + ) + diff --git a/dislord/model/base.py b/dislord/model/base.py new file mode 100644 index 00000000..595a11b0 --- /dev/null +++ b/dislord/model/base.py @@ -0,0 +1,242 @@ +import abc +import copy +import json +import typing +from enum import Enum, EnumType, IntEnum +from functools import reduce +from types import UnionType +from typing import get_type_hints, Union, Self + +from dislord.discord.reference import Missing +from dislord.types import ObjDict + + +def cast(obj, type_hint, param_name=None, client=None): + # None + if type_hint is None and (obj is None or obj is Missing): + return obj + + # Missing + if type_hint is Missing and (obj is None or obj is Missing): + return obj + + if type_hint is None: + print("type_hint is None but obj is {}".format(obj)) + return obj + + if getattr(type_hint, '__origin__', None) == typing.Literal: + if obj in type_hint.__args__: + return obj + else: + raise ValueError(f"{obj} is not in {type_hint}") + + # Union + if isinstance(type_hint, UnionType): + errors = [] + for arg in type_hint.__args__: + try: + return cast(obj, arg, param_name, client) + except Exception as err: + errors.append(err) + raise RuntimeError(f"None of the union type hints succeeded {errors} for {param_name}") + + # List + if isinstance(type_hint, list): + return list(obj) + elif getattr(type_hint, '__origin__', None) is list: + return [cast(o, type_hint.__args__[0], param_name, client) for o in obj] + + # Dict + if isinstance(type_hint, dict): + return dict(obj) + elif getattr(type_hint, '__origin__', None) is dict: + return { + cast(ok, type_hint.__args__[0], param_name, client): cast(obj.get(ok), type_hint.__args__[1], ok, + client) + for ok in obj.keys()} + + # Literal + if not isinstance(type_hint, type): + if obj == type_hint: + return obj + else: + raise RuntimeError(f"{param_name}={obj} is not equal to Literal {type_hint}") + + # BaseModel + if issubclass(type_hint, BaseModel): + return type_hint.from_dict(obj, client) + + if issubclass(type_hint, ObjDict): + if not isinstance(obj, dict): + raise TypeError(f"{type_hint} is not a dict") + return type_hint(**{k: cast(v, + get_type_hints(type_hint).get(k), k, client) + for k, v in obj.items()}) + + # Enum, str, int, float + if issubclass(type_hint, IntEnum): + return type_hint(int(obj)) + + if obj is not None and not isinstance(obj, Missing): + return type_hint(obj) + else: + raise RuntimeError(f"{obj} cannot be converted to {type_hint}") + + +class AutoInitMeta(type): + def __new__(cls, name, bases, dct): + # Extract annotations (type hints) + annotations = dct.get("__annotations__", {}) | reduce(lambda a, b: a | b, + [getattr(t, "__annotations__", {}) for t in bases], {}) + + # Define the __init__ method + def __init__(self, *args, **kwargs): + for key, value in annotations.items(): + if key in kwargs: + setattr(self, key, kwargs[key]) + else: + setattr(self, key, None) + + # Assign positional arguments + for key, arg in zip(annotations, args): + setattr(self, key, arg) + + dct['__init__'] = __init__ + + # Create the new class + return super().__new__(cls, name, bases, dct) + + +class BaseModel(metaclass=AutoInitMeta): + def __init__(self, *args, **kwargs): + # Generated by metaclass + pass + + @staticmethod + @abc.abstractmethod + def from_payload(payload: type[dict]) -> type['BaseModel']: + pass + + @classmethod + def from_dict(cls, env, client): + type_hints = get_type_hints(cls) + if cls == type(env): + return env + + params = {} + for p, hint in type_hints.items(): + if isinstance(env, dict): + prop = env.get(p, Missing()) + else: + prop = getattr(env, p, Missing()) + params[p] = cast(prop, hint, p) + obj = cls(**params) # noqa + obj.client = client + return obj + + @classmethod + def from_kwargs(cls, *, client=None, **kwargs): + return cls.from_dict(kwargs, client) + + def asdict(self, *, dict_factory=dict): + """Return the fields of a dataclass instance as a new dictionary mapping + field names to field values. + + Example usage:: + + class C: + x: int + y: int + + c = C(1, 2) + assert asdict(c) == {'x': 1, 'y': 2} + + If given, 'dict_factory' will be used instead of built-in dict. + The function applies recursively to field values that are + dataclass instances. This will also look into built-in containers: + tuples, lists, and dicts. + """ + return BaseModel._asdict_inner(self, dict_factory) + + @staticmethod + def _asdict_inner(obj, dict_factory): + if obj is Missing: + return None + if isinstance(obj, BaseModel): + result = [] + for name, value in obj.__annotations__.items(): + result.append((BaseModel._asdict_inner(name, dict_factory), + BaseModel._asdict_inner(getattr(obj, name), dict_factory))) + return dict_factory(result) + elif isinstance(obj, tuple) and hasattr(obj, '_fields'): + # obj is a namedtuple. Recurse into it, but the returned + # object is another namedtuple of the same type. This is + # similar to how other list- or tuple-derived classes are + # treated (see below), but we just need to create them + # differently because a namedtuple's __init__ needs to be + # called differently (see bpo-34363). + + # I'm not using namedtuple's _asdict() + # method, because: + # - it does not recurse in to the namedtuple fields and + # convert them to dicts (using dict_factory). + # - I don't actually want to return a dict here. The main + # use case here is json.dumps, and it handles converting + # namedtuples to lists. Admittedly we're losing some + # information here when we produce a json list instead of a + # dict. Note that if we returned dicts here instead of + # namedtuples, we could no longer call asdict() on a data + # structure where a namedtuple was used as a dict key. + + return type(obj)(*[BaseModel._asdict_inner(v, dict_factory) for v in obj]) + elif isinstance(obj, (list, tuple)): + # Assume we can create an object of this type by passing in a + # generator (which is not true for namedtuples, handled + # above). + return type(obj)(BaseModel._asdict_inner(v, dict_factory) for v in obj) + elif isinstance(obj, dict): + return type(obj)((BaseModel._asdict_inner(k, dict_factory), + BaseModel._asdict_inner(v, dict_factory)) + for k, v in obj.items()) + else: + return copy.deepcopy(obj) + + def __eq__(self, other): + result = True + for eq_attr in get_type_hints(self.__class__): + self_attr = getattr(self, eq_attr, None) + other_attr = getattr(other, eq_attr, None) + result = result and compare_missing_none(self_attr, other_attr) + return result + + +class EnhancedJSONEncoder(json.JSONEncoder): + def default(self, o): + if issubclass(type(o), ObjDict): + return {k: self.default(v) for k, v in o.items()} + if issubclass(type(o), BaseModel): + return o.asdict() + if issubclass(type(o), Enum): + return o.value + if o is Missing: + return None + if isinstance(o, list): + return [self.default(v) for v in o] + # if isinstance(o, type): + return f"DEBUG: {o}" + # return super().default(o) + + +def compare_missing_none(obj1, obj2): + obj1_is_none_or_missing = obj1 is None or obj1 is Missing + obj2_is_none_or_missing = obj2 is None or obj2 is Missing + if obj1_is_none_or_missing and obj2_is_none_or_missing: + return True + else: + if isinstance(obj1, Enum) and isinstance(obj2, Enum): + return obj1.value == obj2.value + else: + return obj1.__eq__(obj2) + + +HexColor = type(int) diff --git a/dislord/model/channel.py b/dislord/model/channel.py new file mode 100644 index 00000000..f7511275 --- /dev/null +++ b/dislord/model/channel.py @@ -0,0 +1,12 @@ +from dislord.discord.resources.channel.channel import Channel as ChannelPayload +from dislord.model.base import BaseModel + + +class Channel(BaseModel): + payload: ChannelPayload + + @staticmethod + def from_payload(payload: ChannelPayload) -> 'Channel': + return Channel( + payload=payload + ) diff --git a/dislord/model/commands.py b/dislord/model/commands.py new file mode 100644 index 00000000..9f86d3c1 --- /dev/null +++ b/dislord/model/commands.py @@ -0,0 +1,35 @@ +from discord import Permissions + +from dislord.discord.interactions.application_commands.enums import ApplicationCommandType +from dislord.discord.interactions.application_commands.models import ApplicationCommand as ApplicationCommandPayload, \ + ApplicationCommandOption +from dislord.discord.interactions.receiving_and_responding.interaction import InteractionContextType +from dislord.discord.reference import Snowflake, Missing, Locale +from dislord.discord.resources.application.enums import ApplicationIntegrationType +from dislord.model.base import BaseModel + + +class ApplicationCommand(ApplicationCommandPayload): + + @staticmethod + def from_payload(payload: ApplicationCommandPayload) -> 'ApplicationCommand': + return ApplicationCommand( + **payload + ) + + def to_payload(self) -> ApplicationCommandPayload: + return self + + def __eq__(self, other): + eq_list = ['guild_id', 'name', 'description', 'type', 'name_localization', 'description_localizations', + 'options', 'default_member_permissions', 'dm_permission', 'default_permission', 'nsfw'] + result = True + for eq_attr in eq_list: + self_attr = getattr(self, eq_attr, None) + other_attr = getattr(other, eq_attr, None) + result = result and (self_attr == other_attr or self_attr is other_attr) # compare_missing_none(self_attr, other_attr) + return result + + def __post_init__(self): + if self.guild_id is not None and self.guild_id is not Missing: + self.dm_permission = None diff --git a/dislord/model/components.py b/dislord/model/components.py new file mode 100644 index 00000000..e69de29b diff --git a/dislord/model/guild.py b/dislord/model/guild.py new file mode 100644 index 00000000..0da2b408 --- /dev/null +++ b/dislord/model/guild.py @@ -0,0 +1,13 @@ +from dislord.discord.resources.guild.guild import Guild as GuildPayload +from dislord.model.base import BaseModel + + +class Guild(GuildPayload): + + @staticmethod + def from_payload(payload: GuildPayload) -> 'Guild': + return Guild( + **payload + ) + + diff --git a/dislord/model/user.py b/dislord/model/user.py new file mode 100644 index 00000000..c841f6ae --- /dev/null +++ b/dislord/model/user.py @@ -0,0 +1,14 @@ +from typing import Self + +from dislord.discord.resources.user.user import User as UserPayload +from dislord.model.base import BaseModel + + +class User(BaseModel): + payload: UserPayload + + @staticmethod + def from_payload(payload: UserPayload) -> 'User': + return User( + payload=payload + ) diff --git a/dislord/server.py b/dislord/server.py new file mode 100644 index 00000000..ff5bf128 --- /dev/null +++ b/dislord/server.py @@ -0,0 +1,42 @@ +from .client import ApplicationClient + +try: + from flask import Flask, request + app = Flask(__name__) +except ImportError: + Flask = None + request = None + + class FakeFlask: + @staticmethod + def route(self, *args, **kwargs): # noqa + def decorator(func): + return func + return decorator + + def run(self, *args, **kwargs): + raise RuntimeError("flask library needed in order to use server") + + app = FakeFlask() + +__application_client: ApplicationClient + + +@app.route("/", methods=["POST"]) +async def interactions_endpoint(): + raw_request = request.json + signature = request.headers.get('X-Signature-Ed25519') + timestamp = request.headers.get('X-Signature-Timestamp') + print(f"👉 Request: {raw_request}") + response = __application_client.verified_interact(raw_request, signature, timestamp).as_server_response() + print(f"🫴 Response: {response}") + return response + + +def start_server(application_client, **kwargs): + global __application_client + __application_client = application_client + app.run(**kwargs) + + + diff --git a/dislord/types.py b/dislord/types.py new file mode 100644 index 00000000..0809cc5d --- /dev/null +++ b/dislord/types.py @@ -0,0 +1,18 @@ +class ObjDict(dict): + def __getattr__(self, name): + try: + return self.get(name) + except KeyError: + raise AttributeError(f"'ObjDict' object has no attribute '{name}'") + + def __setattr__(self, name, value): + self[name] = value + + def __eq__(self, other): + if isinstance(other, ObjDict): + for k, v in self.items(): + if v != other.get(k): + return False + return True + else: + return False diff --git a/kb2/__init__.py b/kb2/__init__.py new file mode 100644 index 00000000..9786302c --- /dev/null +++ b/kb2/__init__.py @@ -0,0 +1 @@ +from . import ext diff --git a/kb2/ext/__init__.py b/kb2/ext/__init__.py new file mode 100644 index 00000000..18541b15 --- /dev/null +++ b/kb2/ext/__init__.py @@ -0,0 +1 @@ +from . import koala diff --git a/kb2/ext/koala/__init__.py b/kb2/ext/koala/__init__.py new file mode 100644 index 00000000..9f215bda --- /dev/null +++ b/kb2/ext/koala/__init__.py @@ -0,0 +1 @@ +from . import commands diff --git a/kb2/ext/koala/api.py b/kb2/ext/koala/api.py new file mode 100644 index 00000000..e69de29b diff --git a/kb2/ext/koala/commands.py b/kb2/ext/koala/commands.py new file mode 100644 index 00000000..9a1231a4 --- /dev/null +++ b/kb2/ext/koala/commands.py @@ -0,0 +1,15 @@ +from dislord import CommandGroup +from dislord.discord.interactions.receiving_and_responding.interaction import Interaction +from dislord.discord.interactions.receiving_and_responding.interaction_response import InteractionResponse +from kb2.main import client + +koala_group = CommandGroup(name="koala", description="KoalaBot Base Commands") + + +@koala_group.command(name="support", description="KoalaBot Support server link") +def support(interaction: Interaction): + return InteractionResponse.message( + content="Join our support server for more help! https://discord.gg/5etEjVd") + + +client.register_group(koala_group) diff --git a/kb2/ext/koala/core.py b/kb2/ext/koala/core.py new file mode 100644 index 00000000..9b48f21f --- /dev/null +++ b/kb2/ext/koala/core.py @@ -0,0 +1,4 @@ + + +def support(): + pass \ No newline at end of file diff --git a/kb2/ext/verify/__init__.py b/kb2/ext/verify/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kb2/ext/verify/commands.py b/kb2/ext/verify/commands.py new file mode 100644 index 00000000..016b71ef --- /dev/null +++ b/kb2/ext/verify/commands.py @@ -0,0 +1,5 @@ +from dislord import CommandGroup +from kb2.main import client + +verify_group = CommandGroup(name="verify", description="Configure Guild Verification") +client.register_group(verify_group) diff --git a/kb2/ext/verifyme/__init__.py b/kb2/ext/verifyme/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kb2/ext/verifyme/commands.py b/kb2/ext/verifyme/commands.py new file mode 100644 index 00000000..45eb2133 --- /dev/null +++ b/kb2/ext/verifyme/commands.py @@ -0,0 +1,11 @@ +from dislord.discord.interactions.components.enums import ButtonStyle +from dislord.discord.interactions.components.models import Component, ActionRow, Button +from dislord.discord.interactions.receiving_and_responding.interaction_response import InteractionResponse +from kb2.main import client +from dislord.discord.interactions.receiving_and_responding.interaction import Interaction + + +@client.command(name="verifyme", description="Verify your identity with Koala") +def verify_me(interaction: Interaction): + return InteractionResponse.message(content="Please verify yourself through the below link", components=[ActionRow( + components=[Button(style=ButtonStyle.LINK, label="Verify", custom_id="1", url="https://koalabot.uk")])]) diff --git a/kb2/main.py b/kb2/main.py new file mode 100644 index 00000000..04a55a25 --- /dev/null +++ b/kb2/main.py @@ -0,0 +1,23 @@ +import os + +import dislord + +PUBLIC_KEY = os.environ.get("PUBLIC_KEY") +BOT_TOKEN = os.environ.get("DISCORD_TOKEN") + +client = dislord.ApplicationClient(PUBLIC_KEY, BOT_TOKEN) + +def serverless_handler(event, context): # Not needed if using server + return client.serverless_handler(event, context) + + +def sync_serverless_handler(event, context): + client.sync_commands() + client.sync_commands(guild_ids=[g.id for g in client.guilds]) + return {"statusCode": 200} + + +if __name__ == '__main__': # Not needed if using serverless + client.sync_commands() + client.sync_commands(guild_ids=[g.id for g in client.guilds]) + dislord.server.start_server(client, host='0.0.0.0', debug=True, port=8123) diff --git a/koala/cogs/base/cog.py b/koala/cogs/base/cog.py index 73c7abb9..c2fccc99 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 @@ -36,19 +35,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 +160,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): @@ -257,5 +235,4 @@ async def setup(bot: koalabot) -> None: :param bot: the bot client for KoalaBot """ await bot.add_cog(BaseCog(bot)) - await bot.add_cog(BaseCogSlash()) logger.info("BaseCog is ready.") 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/koalabot.py b/koalabot.py index 143731ce..390fe4e6 100644 --- a/koalabot.py +++ b/koalabot.py @@ -18,13 +18,19 @@ # Built-in/Generic Imports import asyncio import time +from http.client import OK +from typing import Any import discord # Libs from aiohttp import web import aiohttp_cors +from discord import Interaction from discord.ext import commands +from discord.http import Route +from discord.webhook.async_ import async_context +from dislord.discord.interactions.receiving_and_responding.interaction_response import InteractionCallbackType from koala import env # Own modules from koala.db import extension_enabled @@ -32,6 +38,7 @@ from koala.errors import KoalaException from koala.log import logger from koala.utils import error_embed +import kb2.main # Constants COMMAND_PREFIX = "k!" @@ -61,12 +68,25 @@ 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): + interaction = discord.Interaction(data=raw_interaction, state=self._connection) + result = kb2.main.client.interact(raw_interaction) + if result is None or result.status_code != OK: + logger.error("Failed to process interaction: %s", interaction) + return + route = Route( + 'POST', + '/interactions/{webhook_id}/{webhook_token}/callback', + webhook_id=interaction.id, + webhook_token=interaction.token, + ) + await async_context.get().request(route, session=interaction.response._parent._session, payload=result.body) + + 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 +120,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): """ diff --git a/requirements.txt b/requirements.txt index b8b8ff28..c372ab64 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,3 +36,6 @@ urllib3==1.26.14 wcwidth==0.2.6 websockets==10.4 yarl==1.8.2 + +discord-interactions~=0.4.0 +requests~=2.31.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/kb2/__init__.py b/tests/kb2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/kb2/ext/__init__.py b/tests/kb2/ext/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/kb2/ext/koala/__init__.py b/tests/kb2/ext/koala/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/kb2/ext/koala/test_commands.py b/tests/kb2/ext/koala/test_commands.py new file mode 100644 index 00000000..11e9238d --- /dev/null +++ b/tests/kb2/ext/koala/test_commands.py @@ -0,0 +1,8 @@ +from dislord.discord.interactions.receiving_and_responding.interaction_response import InteractionCallbackType +from kb2.ext.koala import commands + + +def test_support(): + response = commands.support(None) + assert response.type == InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE + assert response.data.content == "Join our support server for more help! https://discord.gg/5etEjVd" diff --git a/tests/koala/__init__.py b/tests/koala/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/koala/cogs/__init__.py b/tests/koala/cogs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/koala/cogs/announce/__init__.py b/tests/koala/cogs/announce/__init__.py new file mode 100644 index 00000000..e69de29b 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/koala/cogs/base/__init__.py b/tests/koala/cogs/base/__init__.py new file mode 100644 index 00000000..e69de29b 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/koala/cogs/colour_role/__init__.py b/tests/koala/cogs/colour_role/__init__.py new file mode 100644 index 00000000..e69de29b 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/koala/cogs/insights/__init__.py b/tests/koala/cogs/insights/__init__.py new file mode 100644 index 00000000..e69de29b 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/koala/cogs/intro_cog/__init__.py b/tests/koala/cogs/intro_cog/__init__.py new file mode 100644 index 00000000..e69de29b 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/koala/cogs/react_for_role/__init__.py b/tests/koala/cogs/react_for_role/__init__.py new file mode 100644 index 00000000..e69de29b 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/koala/cogs/text_filter/__init__.py b/tests/koala/cogs/text_filter/__init__.py new file mode 100644 index 00000000..e69de29b 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/koala/cogs/twitch_alert/__init__.py b/tests/koala/cogs/twitch_alert/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/cogs/twitch_alert/test_cog.py b/tests/koala/cogs/twitch_alert/test_cog.py similarity index 99% rename from tests/cogs/twitch_alert/test_cog.py rename to tests/koala/cogs/twitch_alert/test_cog.py index ebd1571a..25b16bac 100644 --- a/tests/cogs/twitch_alert/test_cog.py +++ b/tests/koala/cogs/twitch_alert/test_cog.py @@ -19,7 +19,7 @@ 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 tests.koala.tests_utils.last_ctx_cog import LastCtxCog # Constants DB_PATH = "Koala.db" 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/koala/cogs/verification/__init__.py b/tests/koala/cogs/verification/__init__.py new file mode 100644 index 00000000..e69de29b 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 From 9ddec8a676f99edd5fcd71641c095ccb667654b3 Mon Sep 17 00:00:00 2001 From: Jack Draper Date: Sun, 27 Oct 2024 11:01:59 +0000 Subject: [PATCH 10/16] feat: integration with KB2 lambda --- .../versions/51ccdeec4499_kb2_extensions.py | 90 +++++++ devdocs/KB2.md | 15 +- dislord/__init__.py | 10 - dislord/api.py | 71 ----- dislord/client.py | 179 ------------- dislord/discord/__init__.py | 1 - dislord/discord/interactions/__init__.py | 0 .../application_commands/__init__.py | 0 .../application_commands/enums.py | 44 ---- .../application_commands/models.py | 69 ----- .../interactions/components/__init__.py | 0 .../discord/interactions/components/enums.py | 25 -- .../discord/interactions/components/models.py | 60 ----- .../receiving_and_responding/__init__.py | 0 .../receiving_and_responding/interaction.py | 66 ----- .../interaction_data.py | 54 ---- .../interaction_response.py | 61 ----- .../message_interaction.py | 22 -- dislord/discord/reference.py | 63 ----- dislord/discord/resources/__init__.py | 0 .../discord/resources/application/__init__.py | 0 .../discord/resources/application/enums.py | 6 - .../discord/resources/application/flags.py | 14 - .../discord/resources/application/models.py | 49 ---- .../__init__.py | 0 .../enums.py | 12 - .../models.py | 12 - .../discord/resources/audit_log/__init__.py | 0 dislord/discord/resources/audit_log/enums.py | 69 ----- dislord/discord/resources/audit_log/models.py | 48 ---- .../resources/auto_moderation/__init__.py | 0 .../resources/auto_moderation/enums.py | 24 -- .../resources/auto_moderation/models.py | 36 --- dislord/discord/resources/channel/__init__.py | 0 .../resources/channel/allowed_mentions.py | 17 -- .../discord/resources/channel/attachment.py | 27 -- dislord/discord/resources/channel/channel.py | 89 ------- .../resources/channel/channel_mention.py | 10 - .../resources/channel/default_reaction.py | 8 - dislord/discord/resources/channel/embed.py | 63 ----- dislord/discord/resources/channel/enums.py | 5 - .../resources/channel/followed_channel.py | 7 - .../discord/resources/channel/forum_tag.py | 10 - dislord/discord/resources/channel/message.py | 66 ----- .../discord/resources/channel/message_call.py | 7 - .../channel/message_interaction_metadata.py | 17 -- .../resources/channel/message_reference.py | 9 - .../discord/resources/channel/overwrite.py | 16 -- .../resources/channel/partial_message.py | 61 ----- dislord/discord/resources/channel/reaction.py | 13 - .../channel/reaction_count_details.py | 6 - .../channel/role_subscription_data.py | 10 - .../resources/channel/thread_member.py | 13 - .../resources/channel/thread_metadata.py | 11 - dislord/discord/resources/emoji/__init__.py | 0 dislord/discord/resources/emoji/emoji.py | 17 -- dislord/discord/resources/guild/__init__.py | 0 dislord/discord/resources/guild/ban.py | 8 - dislord/discord/resources/guild/guild.py | 141 ---------- .../discord/resources/guild/guild_member.py | 32 --- .../resources/guild/guild_onboarding.py | 45 ---- .../discord/resources/guild/guild_preview.py | 19 -- .../discord/resources/guild/guild_widget.py | 13 - .../resources/guild/guild_widget_settings.py | 7 - .../discord/resources/guild/integration.py | 31 --- .../resources/guild/integration_account.py | 6 - .../guild/integration_application.py | 11 - .../discord/resources/guild/welcome_screen.py | 14 - .../guild_scheduled_event/__init__.py | 0 .../guild_scheduled_event.py | 45 ---- .../guild_scheduled_event_user.py | 10 - .../resources/guild_template/__init__.py | 0 .../guild_template/guild_template.py | 18 -- dislord/discord/resources/invite/__init__.py | 0 dislord/discord/resources/invite/invite.py | 37 --- .../resources/invite/invite_metadata.py | 10 - .../resources/invite/invite_stage_instance.py | 9 - dislord/discord/resources/poll/__init__.py | 0 dislord/discord/resources/poll/layout_type.py | 5 - dislord/discord/resources/poll/poll.py | 15 -- dislord/discord/resources/poll/poll_answer.py | 7 - .../resources/poll/poll_create_request.py | 13 - dislord/discord/resources/poll/poll_media.py | 8 - .../discord/resources/poll/poll_results.py | 12 - .../resources/stage_instance/__init__.py | 0 .../stage_instance/stage_instance.py | 17 -- dislord/discord/resources/sticker/__init__.py | 0 dislord/discord/resources/sticker/sticker.py | 32 --- .../discord/resources/sticker/sticker_item.py | 9 - .../discord/resources/sticker/sticker_pack.py | 13 - dislord/discord/resources/user/__init__.py | 0 .../user/application_role_connection.py | 8 - .../resources/user/avatar_decoration_data.py | 7 - dislord/discord/resources/user/connection.py | 23 -- dislord/discord/resources/user/user.py | 52 ---- dislord/discord/resources/voice/__init__.py | 0 .../discord/resources/voice/voice_region.py | 9 - .../discord/resources/voice/voice_state.py | 19 -- dislord/discord/resources/webhook/__init__.py | 0 dislord/discord/resources/webhook/webhook.py | 27 -- dislord/discord/topics/__init__.py | 0 .../discord/topics/permissions/__init__.py | 0 .../discord/topics/permissions/permissions.py | 52 ---- dislord/discord/topics/permissions/role.py | 32 --- dislord/discord/topics/teams/__init__.py | 0 dislord/discord/topics/teams/data_models.py | 26 -- .../discord/topics/teams/team_member_roles.py | 8 - dislord/dpyadapter/__init__.py | 1 - dislord/dpyadapter/handlers.py | 15 -- dislord/error.py | 6 - dislord/group.py | 47 ---- dislord/model/__init__.py | 0 dislord/model/api.py | 30 --- dislord/model/application.py | 13 - dislord/model/base.py | 242 ------------------ dislord/model/channel.py | 12 - dislord/model/commands.py | 35 --- dislord/model/components.py | 0 dislord/model/guild.py | 13 - dislord/model/user.py | 14 - dislord/server.py | 42 --- dislord/types.py | 18 -- kb2/__init__.py | 1 - kb2/ext/__init__.py | 1 - kb2/ext/koala/__init__.py | 1 - kb2/ext/koala/api.py | 0 kb2/ext/koala/commands.py | 15 -- kb2/ext/koala/core.py | 4 - kb2/ext/verify/__init__.py | 0 kb2/ext/verify/commands.py | 5 - kb2/ext/verifyme/__init__.py | 0 kb2/ext/verifyme/commands.py | 11 - kb2/main.py | 23 -- koala/cogs/announce/cog.py | 62 +++-- koala/cogs/base/cog.py | 39 ++- koala/cogs/colour_role/cog.py | 46 ++-- koala/cogs/react_for_role/cog.py | 74 +++--- koala/cogs/text_filter/cog.py | 53 ++-- koala/cogs/twitch_alert/cog.py | 44 ++-- koala/cogs/verification/cog.py | 47 ++-- koala/cogs/voting/cog.py | 74 +++--- koala/env.py | 4 + koala/kb2/__init__.py | 2 + koala/kb2/adapter.py | 58 +++++ koala/kb2/env.py | 11 + koala/kb2/log.py | 3 + koala/kb2/models.py | 32 +++ koala/log.py | 4 +- koalabot.py | 73 +++--- requirements.txt | 6 +- tests/kb2/__init__.py | 0 tests/kb2/ext/__init__.py | 0 tests/kb2/ext/koala/__init__.py | 0 tests/kb2/ext/koala/test_commands.py | 8 - tests/pytest.ini | 1 + 155 files changed, 519 insertions(+), 3103 deletions(-) create mode 100644 alembic/versions/51ccdeec4499_kb2_extensions.py delete mode 100644 dislord/__init__.py delete mode 100644 dislord/api.py delete mode 100644 dislord/client.py delete mode 100644 dislord/discord/__init__.py delete mode 100644 dislord/discord/interactions/__init__.py delete mode 100644 dislord/discord/interactions/application_commands/__init__.py delete mode 100644 dislord/discord/interactions/application_commands/enums.py delete mode 100644 dislord/discord/interactions/application_commands/models.py delete mode 100644 dislord/discord/interactions/components/__init__.py delete mode 100644 dislord/discord/interactions/components/enums.py delete mode 100644 dislord/discord/interactions/components/models.py delete mode 100644 dislord/discord/interactions/receiving_and_responding/__init__.py delete mode 100644 dislord/discord/interactions/receiving_and_responding/interaction.py delete mode 100644 dislord/discord/interactions/receiving_and_responding/interaction_data.py delete mode 100644 dislord/discord/interactions/receiving_and_responding/interaction_response.py delete mode 100644 dislord/discord/interactions/receiving_and_responding/message_interaction.py delete mode 100644 dislord/discord/reference.py delete mode 100644 dislord/discord/resources/__init__.py delete mode 100644 dislord/discord/resources/application/__init__.py delete mode 100644 dislord/discord/resources/application/enums.py delete mode 100644 dislord/discord/resources/application/flags.py delete mode 100644 dislord/discord/resources/application/models.py delete mode 100644 dislord/discord/resources/application_role_connection_metadata/__init__.py delete mode 100644 dislord/discord/resources/application_role_connection_metadata/enums.py delete mode 100644 dislord/discord/resources/application_role_connection_metadata/models.py delete mode 100644 dislord/discord/resources/audit_log/__init__.py delete mode 100644 dislord/discord/resources/audit_log/enums.py delete mode 100644 dislord/discord/resources/audit_log/models.py delete mode 100644 dislord/discord/resources/auto_moderation/__init__.py delete mode 100644 dislord/discord/resources/auto_moderation/enums.py delete mode 100644 dislord/discord/resources/auto_moderation/models.py delete mode 100644 dislord/discord/resources/channel/__init__.py delete mode 100644 dislord/discord/resources/channel/allowed_mentions.py delete mode 100644 dislord/discord/resources/channel/attachment.py delete mode 100644 dislord/discord/resources/channel/channel.py delete mode 100644 dislord/discord/resources/channel/channel_mention.py delete mode 100644 dislord/discord/resources/channel/default_reaction.py delete mode 100644 dislord/discord/resources/channel/embed.py delete mode 100644 dislord/discord/resources/channel/enums.py delete mode 100644 dislord/discord/resources/channel/followed_channel.py delete mode 100644 dislord/discord/resources/channel/forum_tag.py delete mode 100644 dislord/discord/resources/channel/message.py delete mode 100644 dislord/discord/resources/channel/message_call.py delete mode 100644 dislord/discord/resources/channel/message_interaction_metadata.py delete mode 100644 dislord/discord/resources/channel/message_reference.py delete mode 100644 dislord/discord/resources/channel/overwrite.py delete mode 100644 dislord/discord/resources/channel/partial_message.py delete mode 100644 dislord/discord/resources/channel/reaction.py delete mode 100644 dislord/discord/resources/channel/reaction_count_details.py delete mode 100644 dislord/discord/resources/channel/role_subscription_data.py delete mode 100644 dislord/discord/resources/channel/thread_member.py delete mode 100644 dislord/discord/resources/channel/thread_metadata.py delete mode 100644 dislord/discord/resources/emoji/__init__.py delete mode 100644 dislord/discord/resources/emoji/emoji.py delete mode 100644 dislord/discord/resources/guild/__init__.py delete mode 100644 dislord/discord/resources/guild/ban.py delete mode 100644 dislord/discord/resources/guild/guild.py delete mode 100644 dislord/discord/resources/guild/guild_member.py delete mode 100644 dislord/discord/resources/guild/guild_onboarding.py delete mode 100644 dislord/discord/resources/guild/guild_preview.py delete mode 100644 dislord/discord/resources/guild/guild_widget.py delete mode 100644 dislord/discord/resources/guild/guild_widget_settings.py delete mode 100644 dislord/discord/resources/guild/integration.py delete mode 100644 dislord/discord/resources/guild/integration_account.py delete mode 100644 dislord/discord/resources/guild/integration_application.py delete mode 100644 dislord/discord/resources/guild/welcome_screen.py delete mode 100644 dislord/discord/resources/guild_scheduled_event/__init__.py delete mode 100644 dislord/discord/resources/guild_scheduled_event/guild_scheduled_event.py delete mode 100644 dislord/discord/resources/guild_scheduled_event/guild_scheduled_event_user.py delete mode 100644 dislord/discord/resources/guild_template/__init__.py delete mode 100644 dislord/discord/resources/guild_template/guild_template.py delete mode 100644 dislord/discord/resources/invite/__init__.py delete mode 100644 dislord/discord/resources/invite/invite.py delete mode 100644 dislord/discord/resources/invite/invite_metadata.py delete mode 100644 dislord/discord/resources/invite/invite_stage_instance.py delete mode 100644 dislord/discord/resources/poll/__init__.py delete mode 100644 dislord/discord/resources/poll/layout_type.py delete mode 100644 dislord/discord/resources/poll/poll.py delete mode 100644 dislord/discord/resources/poll/poll_answer.py delete mode 100644 dislord/discord/resources/poll/poll_create_request.py delete mode 100644 dislord/discord/resources/poll/poll_media.py delete mode 100644 dislord/discord/resources/poll/poll_results.py delete mode 100644 dislord/discord/resources/stage_instance/__init__.py delete mode 100644 dislord/discord/resources/stage_instance/stage_instance.py delete mode 100644 dislord/discord/resources/sticker/__init__.py delete mode 100644 dislord/discord/resources/sticker/sticker.py delete mode 100644 dislord/discord/resources/sticker/sticker_item.py delete mode 100644 dislord/discord/resources/sticker/sticker_pack.py delete mode 100644 dislord/discord/resources/user/__init__.py delete mode 100644 dislord/discord/resources/user/application_role_connection.py delete mode 100644 dislord/discord/resources/user/avatar_decoration_data.py delete mode 100644 dislord/discord/resources/user/connection.py delete mode 100644 dislord/discord/resources/user/user.py delete mode 100644 dislord/discord/resources/voice/__init__.py delete mode 100644 dislord/discord/resources/voice/voice_region.py delete mode 100644 dislord/discord/resources/voice/voice_state.py delete mode 100644 dislord/discord/resources/webhook/__init__.py delete mode 100644 dislord/discord/resources/webhook/webhook.py delete mode 100644 dislord/discord/topics/__init__.py delete mode 100644 dislord/discord/topics/permissions/__init__.py delete mode 100644 dislord/discord/topics/permissions/permissions.py delete mode 100644 dislord/discord/topics/permissions/role.py delete mode 100644 dislord/discord/topics/teams/__init__.py delete mode 100644 dislord/discord/topics/teams/data_models.py delete mode 100644 dislord/discord/topics/teams/team_member_roles.py delete mode 100644 dislord/dpyadapter/__init__.py delete mode 100644 dislord/dpyadapter/handlers.py delete mode 100644 dislord/error.py delete mode 100644 dislord/group.py delete mode 100644 dislord/model/__init__.py delete mode 100644 dislord/model/api.py delete mode 100644 dislord/model/application.py delete mode 100644 dislord/model/base.py delete mode 100644 dislord/model/channel.py delete mode 100644 dislord/model/commands.py delete mode 100644 dislord/model/components.py delete mode 100644 dislord/model/guild.py delete mode 100644 dislord/model/user.py delete mode 100644 dislord/server.py delete mode 100644 dislord/types.py delete mode 100644 kb2/__init__.py delete mode 100644 kb2/ext/__init__.py delete mode 100644 kb2/ext/koala/__init__.py delete mode 100644 kb2/ext/koala/api.py delete mode 100644 kb2/ext/koala/commands.py delete mode 100644 kb2/ext/koala/core.py delete mode 100644 kb2/ext/verify/__init__.py delete mode 100644 kb2/ext/verify/commands.py delete mode 100644 kb2/ext/verifyme/__init__.py delete mode 100644 kb2/ext/verifyme/commands.py delete mode 100644 kb2/main.py create mode 100644 koala/kb2/__init__.py create mode 100644 koala/kb2/adapter.py create mode 100644 koala/kb2/env.py create mode 100644 koala/kb2/log.py create mode 100644 koala/kb2/models.py delete mode 100644 tests/kb2/__init__.py delete mode 100644 tests/kb2/ext/__init__.py delete mode 100644 tests/kb2/ext/koala/__init__.py delete mode 100644 tests/kb2/ext/koala/test_commands.py 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/devdocs/KB2.md b/devdocs/KB2.md index 3c6f0e8b..09704ebd 100644 --- a/devdocs/KB2.md +++ b/devdocs/KB2.md @@ -4,17 +4,13 @@ Migrating to Serverless Architecture utilising DynamoDB. # Base ## Bot Owner -- `/owner koala reload` (v1 only) - Reload discord.py extensions -- `/owner koala ping` - Get ping -- `/owner koala activity` - Manage current and scheduled +- `/owner koala version` ## Guild Admin -- `/koala extension` - show and enable/disable extensions -- `/koala clear ` - Delete messages in the channel +- `/koala extensions` - show and enable/disable extensions ## Guild Member - `/koala support` -- `/koala version` # Verify ## Bot Owner @@ -24,4 +20,9 @@ Migrating to Serverless Architecture utilising DynamoDB. - `/verify enable` ## Guild Member -- `/verifyme` \ No newline at end of file +- `/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/dislord/__init__.py b/dislord/__init__.py deleted file mode 100644 index a2fc5a3d..00000000 --- a/dislord/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from . import api, client, error, server -from .discord import interactions -from .client import ApplicationClient -from .group import CommandGroup - - -try: - from . import dpyadapter -except ImportError: - pass # dpy not configured diff --git a/dislord/api.py b/dislord/api.py deleted file mode 100644 index 6878eb15..00000000 --- a/dislord/api.py +++ /dev/null @@ -1,71 +0,0 @@ -import json -from time import sleep - -import requests - -from .discord.reference import DISCORD_URL -from .error import DiscordApiException -from .model.base import cast, EnhancedJSONEncoder - - -# WARNING: Average time to call and get response from API is 25ms, not great to call lots if you want quick processing - - -class DiscordApi: - def __init__(self, client, bot_token): - self.client = client - self.bot_token = bot_token - self.auth_header = {"Authorization": "Bot " + self.bot_token} - - def get(self, endpoint: str, params: dict = None, type_hint: type = None, **kwargs): - print(f"📨 Sending to Discord API GET: {endpoint}, {params}") - response = requests.get(DISCORD_URL + endpoint, params, **kwargs, headers=self.auth_header) - if response.ok: - print(f"📬 Response from Discord API: {response.content}") - response_payload = json.loads(response.content) - if type_hint: - return cast(response_payload, type_hint, client=self.client) - else: - return response_payload - elif response.status_code == 429: - retry_after = response.json()["retry_after"] - print(f"⚠️ Rate Limited, waiting {retry_after}s") - sleep(retry_after) - return self.get(endpoint, params, type_hint, **kwargs) - else: - raise DiscordApiException(f"{response.status_code} {response.text} error when calling discord API " - f"URL: GET {endpoint} Params: {params}") - - def delete(self, endpoint: str, **kwargs): - print(f"📨 Sending to Discord API DELETE: {endpoint}") - response = requests.delete(DISCORD_URL + endpoint, **kwargs, headers=self.auth_header) - if response.ok: - print(f"📬 Response from Discord API: {response.content}") - return - elif response.status_code == 429: - retry_after = response.json()["retry_after"] - print(f"⚠️ Rate Limited, waiting {retry_after}s") - sleep(retry_after) - return self.delete(endpoint, **kwargs) - else: - raise DiscordApiException(f"{response.status_code} {response.text} error when calling discord API " - f"URL: DELETE {endpoint}") - - def post(self, endpoint: str, body: object = None, type_hint: type = None, **kwargs): - body_json = json.dumps(body, cls=EnhancedJSONEncoder) - print(f"📨 Sending to Discord API POST: {endpoint}, {body_json}") - headers = self.auth_header - headers["Content-Type"] = "application/json" - response = requests.post(DISCORD_URL + endpoint, data=body_json, - **kwargs, headers=headers) - if response.ok: - print(f"📬 Response from Discord API: {response.content}") - return cast(json.loads(response.content), type_hint, client=self.client) - elif response.status_code == 429: - retry_after = response.json()["retry_after"] - print(f"⚠️ Rate Limited, waiting {retry_after}s") - sleep(retry_after) - return self.post(endpoint, body, type_hint, **kwargs) - else: - raise DiscordApiException(f"{response.status_code} {response.text} error when calling discord API " - f"URL: POST {endpoint} Body: {body_json}") diff --git a/dislord/client.py b/dislord/client.py deleted file mode 100644 index c088f5f5..00000000 --- a/dislord/client.py +++ /dev/null @@ -1,179 +0,0 @@ -import json -from typing import Callable - -from discord_interactions import verify_key, InteractionType - -from .discord.interactions.application_commands.enums import ApplicationCommandType -from .discord.interactions.application_commands.models import ApplicationCommandOption -from .discord.interactions.receiving_and_responding.interaction import Interaction -from .discord.interactions.receiving_and_responding.interaction_response import InteractionResponse -from .discord.reference import Snowflake, Missing -from .discord.resources.application.models import Application -from .group import CommandGroup -from .api import DiscordApi -from .error import DiscordApiException -from .model.api import HttpResponse, HttpUnauthorized, HttpOk -from .model.base import cast, EnhancedJSONEncoder -from .model.channel import Channel -from .model.commands import ApplicationCommand -from .model.guild import Guild -from .model.user import User - - -class ApplicationClient: - _public_key: str - _api: DiscordApi - _commands: dict[Snowflake, dict[str, ApplicationCommand]] = {} - _command_callbacks: dict[str, Callable] = {} - _application: Application = Missing() - _guilds: list[Guild] = Missing() - - def __init__(self, public_key, bot_token): - self._public_key = public_key - self._api = DiscordApi(self, bot_token) - - def verified_interact(self, raw_request, signature, timestamp) -> HttpResponse: - if signature is None or timestamp is None or not verify_key(json.dumps(raw_request, separators=(',', ':')) - .encode('utf-8'), signature, timestamp, - self._public_key): - return HttpUnauthorized('Bad request signature') - return self.interact(raw_request) - - def interact(self, raw_request) -> HttpResponse: - interaction = cast(raw_request, Interaction, self) - - if interaction.type == InteractionType.PING: # PING - response_data = InteractionResponse.pong() # PONG - elif interaction.type == InteractionType.APPLICATION_COMMAND: - data = interaction.data - command_name = data.name - kwargs = {} - for option in data.options: - kwargs[option.name] = option.value - response_data = self._command_callbacks[command_name](interaction=interaction, **kwargs) - - else: - raise DiscordApiException(DiscordApiException.UNKNOWN_INTERACTION_TYPE.format(interaction.type)) - - return HttpOk(json.loads(json.dumps(response_data, cls=EnhancedJSONEncoder)), headers={"Content-Type": "application/json"}) - - def add_command(self, command: ApplicationCommand, callback: Callable): - if self._commands.get(command.guild_id) is None: - self._commands[command.guild_id] = {} - self._command_callbacks[command.name] = callback - self._commands.get(command.guild_id)[command.name] = command - - def command(self, *, name, description, dm_permission=True, nsfw=False, guild_ids: list[Snowflake] = None, - options: list[ApplicationCommandOption] = None): - if guild_ids is None: - guild_ids = ["ALL"] - - def decorator(func): - for guild_id in guild_ids: - if guild_id == "ALL": - guild_id = None - self.add_command(ApplicationCommand(name=name, description=description, - type=ApplicationCommandType.CHAT_INPUT, - dm_permission=dm_permission, nsfw=nsfw, - guild_id=guild_id, options=options, client=self), func) - return func - - return decorator - - def register_group(self, command_group: CommandGroup): - if self._commands.get(command_group.guild_id) is None: - self._commands[command_group.guild_id] = {} - - self._commands[command_group.guild_id][command_group.name] = ApplicationCommand( - name=command_group.name, description=command_group.description, type=ApplicationCommandType.CHAT_INPUT, - dm_permission=command_group.dm_permission, nsfw=command_group.nsfw, guild_id=command_group.guild_id, - options=list(command_group.commands.values()), client=self) - - self._command_callbacks[command_group.name] = command_group.callback - - def serverless_handler(self, event, context): - if event['httpMethod'] == "POST": - print(f"🫱 Full Event: {event}") - raw_request = json.loads(event["body"]) - print(f"👉 Request: {raw_request}") - raw_headers = event["headers"] - signature = raw_headers.get('x-signature-ed25519') - timestamp = raw_headers.get('x-signature-timestamp') - response = self.verified_interact(raw_request, signature, timestamp).as_serverless_response() - print(f"🫴 Response: {response}") - return response - - @property - def application(self): - if self._application is Missing(): - self._application = self.get_application() - return self._application - - @property - def guilds(self) -> list[Guild]: - if self._guilds is Missing(): - self._guilds = self._get_guilds() - return self._guilds - - def get_application(self): - return self._api.get("/applications/@me", type_hint=Application) - - def sync_commands(self, guild_id: Snowflake = None, guild_ids: list[Snowflake] = None, - application_id: Snowflake = None): - if guild_ids: - for g_id in guild_ids: - self.sync_commands(guild_id=g_id, application_id=application_id) - - registered_commands = self._get_commands(guild_id) - client_commands = self._commands.get(guild_id) - missing_commands = list(client_commands.values()) if client_commands else [] - for registered_command in registered_commands: - if registered_command not in missing_commands: - self._delete_commands(command_id=registered_command.id, guild_id=guild_id, - application_id=registered_command.application_id) - else: - missing_commands.remove(registered_command) - - for missing_command in missing_commands: - self._register_command(missing_command, guild_id=guild_id, application_id=application_id) - - def _get_commands(self, guild_id: Snowflake = None, application_id: Snowflake = None, - with_localizations: bool = None) -> list[ApplicationCommand]: - endpoint = f"/applications/{application_id if application_id else self.application.id}" - if guild_id: - endpoint += f"/guilds/{guild_id}" - - params = {} - if with_localizations is not None: - params["with_localizations"] = with_localizations - - return [ApplicationCommand(**p) for p in self._api.get(f"{endpoint}/commands", params=params, - type_hint=list[ApplicationCommand])] - - def _delete_commands(self, command_id: Snowflake, - guild_id: Snowflake = None, application_id: Snowflake = None) -> None: - endpoint = f"/applications/{application_id if application_id else self.application.id}" - if guild_id: - endpoint += f"/guilds/{guild_id}" - - self._api.delete(f"{endpoint}/commands/{command_id}") - - def _register_command(self, application_command: ApplicationCommand, - guild_id: Snowflake = None, application_id: Snowflake = None) -> ApplicationCommand: - endpoint = f"/applications/{application_id if application_id else self.application.id}" - if guild_id: - endpoint += f"/guilds/{guild_id}" - return ApplicationCommand(**self._api.post(f"{endpoint}/commands", application_command.to_payload(), - type_hint=ApplicationCommand)) - - def get_user(self, user_id=None) -> User: - return User.from_payload(self._api.get(f"/users/{user_id if user_id else '@me'}")) - - def get_guild(self, guild_id) -> Guild: - return Guild.from_payload(self._api.get(f"/guilds/{guild_id}")) - - def _get_guilds(self) -> list[Guild]: - return [Guild.from_payload(p) for p in self._api.get("/users/@me/guilds")] - - def get_channel(self, channel_id) -> list[Channel]: - return [Channel.from_payload(p) for p in self._api.get(f"/channels/{channel_id}")] diff --git a/dislord/discord/__init__.py b/dislord/discord/__init__.py deleted file mode 100644 index b6e690fd..00000000 --- a/dislord/discord/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import * diff --git a/dislord/discord/interactions/__init__.py b/dislord/discord/interactions/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/interactions/application_commands/__init__.py b/dislord/discord/interactions/application_commands/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/interactions/application_commands/enums.py b/dislord/discord/interactions/application_commands/enums.py deleted file mode 100644 index af263096..00000000 --- a/dislord/discord/interactions/application_commands/enums.py +++ /dev/null @@ -1,44 +0,0 @@ -from enum import Enum - - -class ApplicationCommandType(Enum): - CHAT_INPUT = 1 - USER = 2 - MESSAGE = 3 - - -class ApplicationCommandOptionType(Enum): - SUB_COMMAND = 1 - SUB_COMMAND_GROUP = 2 - STRING = 3 - INTEGER = 4 - BOOLEAN = 5 - USER = 6 - CHANNEL = 7 - ROLE = 8 - MENTIONABLE = 9 - NUMBER = 10 - ATTACHMENT = 11 - - # def from_python_type(self, type_hint): - # python_mapping = {str: self.STRING, int: self.INTEGER, bool: self.BOOLEAN, - # User: self.USER, Channel: self.CHANNEL, - # # Role: self.ROLE, Mentionable: self.MENTIONABLE, FIXME - # float: self.NUMBER, - # # Attachment: self.ATTACHMENT FIXME - # } - # par = python_mapping.get(type_hint) - # if par is None: - # raise RuntimeError(f"Unexpected command param type: {type_hint}") - # - # def __eq__(self, other): - # try: - # return self.value == other.value - # except Exception: - # return False - - -class ApplicationCommandPermissionType(Enum): - ROLE = 1 - USER = 2 - CHANNEL = 3 diff --git a/dislord/discord/interactions/application_commands/models.py b/dislord/discord/interactions/application_commands/models.py deleted file mode 100644 index 1294341c..00000000 --- a/dislord/discord/interactions/application_commands/models.py +++ /dev/null @@ -1,69 +0,0 @@ -from typing import Self, TypedDict - -from discord import Permissions - -from dislord.discord.interactions.application_commands.enums import ApplicationCommandType, \ - ApplicationCommandPermissionType, ApplicationCommandOptionType -from dislord.discord.interactions.receiving_and_responding.interaction import InteractionContextType -from dislord.discord.resources.application.enums import ApplicationIntegrationType -from dislord.discord.resources.channel.channel import ChannelType -from dislord.discord.reference import Snowflake, Missing, Locale -from dislord.types import ObjDict - - -class ApplicationCommandPermissions(ObjDict): - id: Snowflake - type: ApplicationCommandPermissionType - permission: bool - - -class GuildApplicationCommandPermissions(ObjDict): - id: Snowflake - application_id: Snowflake - guild_id: Snowflake - permissions: list[ApplicationCommandPermissions] - - -ApplicationCommandPermissionsObject = GuildApplicationCommandPermissions - - -class ApplicationCommandOptionChoice(ObjDict): - name: str - name_localizations: dict[Locale, str] | Missing | None - value: str | int | float - - -class ApplicationCommandOption(ObjDict): - type: ApplicationCommandOptionType - name: str - name_localizations: dict[Locale, str] | Missing | None - description: str - description_localizations: dict[Locale, str] | Missing | None - required: bool | Missing - choices: list[ApplicationCommandOptionChoice] | Missing - options: list['ApplicationCommandOption'] | Missing - channel_types: list[ChannelType] | Missing - min_value: int | float | Missing - max_values: int | float | Missing - min_length: int | Missing - max_length: int | Missing - autocomplete: bool | Missing - - -class ApplicationCommand(ObjDict): - id: Snowflake - type: ApplicationCommandType | Missing - application_id: Snowflake - guild_id: Snowflake | Missing - name: str - name_localizations: dict[Locale, str] | Missing| None - description: str - description_localizations: dict[Locale, str] | Missing | None - options: list[ApplicationCommandOption] | Missing - default_member_permissions: Permissions | None - dm_permission: bool | Missing # Deprecated - default_permission: bool | Missing | None = True - nsfw: bool | Missing = False - integration_types: list[ApplicationIntegrationType] | Missing = [ApplicationIntegrationType.GUILD_INSTALL] - contexts: list[InteractionContextType] | Missing | None - version: Snowflake \ No newline at end of file diff --git a/dislord/discord/interactions/components/__init__.py b/dislord/discord/interactions/components/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/interactions/components/enums.py b/dislord/discord/interactions/components/enums.py deleted file mode 100644 index 18e2d520..00000000 --- a/dislord/discord/interactions/components/enums.py +++ /dev/null @@ -1,25 +0,0 @@ -from enum import Enum - - -class ComponentType(Enum): - ACTION_ROW = 1 - BUTTON = 2 - STRING_SELECT = 3 - TEXT_INPUT = 4 - USER_SELECT = 5 - ROLE_SELECT = 6 - MENTIONABLE_SELECT = 7 - CHANNEL_SELECT = 8 - - -class ButtonStyle(Enum): - PRIMARY = 1 - SECONDARY = 2 - SUCCESS = 3 - DANGER = 4 - LINK = 5 - - -class TextInputStyle(Enum): - SHORT = 1 - PARAGRAPH = 2 diff --git a/dislord/discord/interactions/components/models.py b/dislord/discord/interactions/components/models.py deleted file mode 100644 index 4fc6822a..00000000 --- a/dislord/discord/interactions/components/models.py +++ /dev/null @@ -1,60 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.interactions.components.enums import ButtonStyle, TextInputStyle, ComponentType -from dislord.discord.resources.channel.channel import ChannelType -from dislord.discord.reference import Snowflake, Missing -from dislord.discord.resources.emoji.emoji import PartialEmoji - - -class TextInput(ObjDict): - type: ComponentType = ComponentType.TEXT_INPUT - custom_id: str - style: TextInputStyle - label: str - min_length: int | Missing - max_length: int | Missing - required: bool | Missing - value: str | Missing - placeholder: str | Missing - - -class SelectDefaultValue(ObjDict): - id: Snowflake - type: str - - -class SelectOption(ObjDict): - label: str - value: str - description: str | Missing - # emoji: PartialEmoji | Missing FIXME - default: bool | Missing - - -class SelectMenu(ObjDict): - type: ComponentType - custom_id: str - options: list[SelectOption] | Missing - channel_types: list[ChannelType] | Missing - placeholder: str | Missing - default_values: list[SelectDefaultValue] | Missing - min_values: int | Missing - max_values: int | Missing - disabled: bool | Missing - - -class Button(ObjDict): - type: ComponentType = ComponentType.BUTTON - style: ButtonStyle - label: str | None - emoji: PartialEmoji | Missing - custom_id: str | Missing - url: str | Missing - disabled: bool | Missing - - -class ActionRow(ObjDict): - type: ComponentType = ComponentType.ACTION_ROW - components: list[Button | SelectMenu | TextInput] - - -Component = ActionRow | Button | SelectMenu | TextInput diff --git a/dislord/discord/interactions/receiving_and_responding/__init__.py b/dislord/discord/interactions/receiving_and_responding/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/interactions/receiving_and_responding/interaction.py b/dislord/discord/interactions/receiving_and_responding/interaction.py deleted file mode 100644 index 75d4cad2..00000000 --- a/dislord/discord/interactions/receiving_and_responding/interaction.py +++ /dev/null @@ -1,66 +0,0 @@ -from enum import Enum -from typing import Literal - -from dislord.types import ObjDict -from dislord.discord.interactions.receiving_and_responding.interaction_data import InteractionData, \ - ApplicationCommandData, MessageComponentData, ModalSubmitData -from dislord.discord.interactions.receiving_and_responding.message_interaction import InteractionType -from dislord.discord.resources.application.enums import ApplicationIntegrationType -from dislord.discord.resources.application.models import ApplicationIntegrationTypeConfiguration -from dislord.discord.resources.channel.channel import PartialChannel -from dislord.discord.resources.channel.message import Message -from dislord.discord.resources.guild.guild import Guild -from dislord.discord.resources.guild.guild_member import GuildMember -from dislord.discord.resources.user.user import User -from dislord.discord.reference import Snowflake, Missing, Locale - - -class InteractionContextType(Enum): - GUILD = 0 - BOT_DM = 1 - PRIVATE_CHANNEL = 2 - - -class __Interaction(ObjDict): - id: Snowflake - application_id: Snowflake - type: InteractionType - data: InteractionData | Missing - guild: Guild | Missing - guild_id: Snowflake | Missing - channel: PartialChannel | Missing - channel_id: Snowflake | Missing - member: GuildMember | Missing - user: User | Missing - token: str - version: int - message: Message | Missing - app_permissions: str - locale: Locale | Missing - guild_locale: Locale | Missing - # entitlements: list[Entitlement] FIXME - authorizing_integration_owners: dict[ApplicationIntegrationType, Snowflake] - context: InteractionContextType | Missing - - -class PingInteraction(__Interaction): - type: Literal[1] - data: Missing - - -class ApplicationCommandInteraction(__Interaction): - type: Literal[2, 4] - data: ApplicationCommandData - - -class MessageInteraction(__Interaction): - type: Literal[3] - data: MessageComponentData - - -class ModalSubmitInteraction(__Interaction): - type: Literal[5] - data: ModalSubmitData - - -Interaction = PingInteraction | ApplicationCommandInteraction | MessageInteraction | ModalSubmitInteraction diff --git a/dislord/discord/interactions/receiving_and_responding/interaction_data.py b/dislord/discord/interactions/receiving_and_responding/interaction_data.py deleted file mode 100644 index 41055a5d..00000000 --- a/dislord/discord/interactions/receiving_and_responding/interaction_data.py +++ /dev/null @@ -1,54 +0,0 @@ -from typing import Self - -from dislord.types import ObjDict -from dislord.discord.interactions.application_commands.enums import ApplicationCommandOptionType -from dislord.discord.interactions.components.models import SelectOption, Component -from dislord.discord.topics.permissions.role import Role -from dislord.discord.resources.channel.attachment import Attachment -from dislord.discord.resources.channel.channel import PartialChannel -from dislord.discord.resources.channel.partial_message import PartialMessage -from dislord.discord.resources.guild.guild_member import PartialGuildMember -from dislord.discord.resources.user.user import User -from dislord.discord.reference import Snowflake, Missing - - -class ApplicationCommandInteractionDataOption(ObjDict): - name: str - type: ApplicationCommandOptionType - value: str | int | float | bool | Missing - options: list[Self] | Missing - focused: bool | Missing - - -class ModalSubmitData(ObjDict): - custom_id: str - components: list[Component] - - -class ResolvedData(ObjDict): - users: dict[Snowflake, User] | Missing - members: dict[Snowflake, PartialGuildMember] | Missing - roles: dict[Snowflake, Role] | Missing - channels: dict[Snowflake, PartialChannel] | Missing - messages: dict[Snowflake, PartialMessage] | Missing - attachments: dict[Snowflake, Attachment] | Missing - - -class MessageComponentData(ObjDict): - custom_id: str - component_type: int - values: list[SelectOption] | Missing - resolved: ResolvedData | Missing - - -class ApplicationCommandData(ObjDict): - id: Snowflake - name: str - type: int - resolved: ResolvedData | Missing - options: list[ApplicationCommandInteractionDataOption] | Missing - guild_id: Snowflake | Missing - target: Snowflake | Missing - - -InteractionData = ApplicationCommandData | MessageComponentData | ModalSubmitData diff --git a/dislord/discord/interactions/receiving_and_responding/interaction_response.py b/dislord/discord/interactions/receiving_and_responding/interaction_response.py deleted file mode 100644 index dd3db255..00000000 --- a/dislord/discord/interactions/receiving_and_responding/interaction_response.py +++ /dev/null @@ -1,61 +0,0 @@ -from enum import Enum - -from dislord.types import ObjDict -from dislord.discord.interactions.application_commands.models import ApplicationCommandOptionChoice -from dislord.discord.interactions.components.models import Component -from dislord.discord.resources.channel.allowed_mentions import AllowedMentions -from dislord.discord.resources.channel.attachment import PartialAttachment -from dislord.discord.resources.channel.embed import Embed -from dislord.discord.resources.channel.message import MessageFlags -from dislord.discord.reference import Missing -from dislord.discord.resources.poll.poll import Poll - - -class ModalInteractionCallbackData(ObjDict): - custom_id: str - title: str - components: list[Component] - - -class AutocompleteInteractionCallbackData(ObjDict): - choices: list[ApplicationCommandOptionChoice] - - -class MessagesInteractionCallbackData(ObjDict): - tts: bool | Missing - content: str | Missing - embeds: list[Embed] | Missing - allowed_mentions: AllowedMentions | Missing - flags: MessageFlags - components: list[Component] | Missing - attachments: list[PartialAttachment] | Missing - poll: Poll | Missing - - -InteractionCallbackData = MessagesInteractionCallbackData | AutocompleteInteractionCallbackData | ModalInteractionCallbackData - - -class InteractionCallbackType(Enum): - PONG = 1 - CHANNEL_MESSAGE_WITH_SOURCE = 4 - DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE = 5 - DEFERRED_UPDATE_MESSAGE = 6 - UPDATE_MESSAGE = 7 # Only valid for component-based receiving_and_responding - APPLICATION_COMMAND_AUTOCOMPLETE_RESULT = 8 - MODAL = 9 # Not available for MODAL_SUBMIT and PING receiving_and_responding. - PREMIUM_REQUIRED = 10 # Not available for APPLICATION_COMMAND_AUTOCOMPLETE and PING receiving_and_responding. - - -class InteractionResponse(ObjDict): - type: InteractionCallbackType - data: InteractionCallbackData | Missing - - @staticmethod - def pong(): - return InteractionResponse(type=InteractionCallbackType.PONG) - - @staticmethod - def message(**kwargs): - cls = InteractionResponse(type=InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE, - data=MessagesInteractionCallbackData(**kwargs)) - return cls diff --git a/dislord/discord/interactions/receiving_and_responding/message_interaction.py b/dislord/discord/interactions/receiving_and_responding/message_interaction.py deleted file mode 100644 index 1b8db585..00000000 --- a/dislord/discord/interactions/receiving_and_responding/message_interaction.py +++ /dev/null @@ -1,22 +0,0 @@ -from enum import Enum - -from dislord.types import ObjDict -from dislord.discord.resources.guild.guild_member import PartialGuildMember -from dislord.discord.resources.user.user import User -from dislord.discord.reference import Missing, Snowflake - - -class InteractionType(Enum): - PING = 1 - APPLICATION_COMMAND = 2 - MESSAGE_COMPONENT = 3 - APPLICATION_COMMAND_AUTOCOMPLETE = 4 - MODAL_SUBMIT = 5 - - -class MessageInteraction(ObjDict): - id: Snowflake - type: InteractionType - name: str - user: User - member: PartialGuildMember | Missing diff --git a/dislord/discord/reference.py b/dislord/discord/reference.py deleted file mode 100644 index 741adb02..00000000 --- a/dislord/discord/reference.py +++ /dev/null @@ -1,63 +0,0 @@ -from enum import Enum - -from dislord.types import ObjDict - -DISCORD_API_VERSION = 10 -DISCORD_URL = f"https://discord.com/api/v{DISCORD_API_VERSION}" - - -class TokenType(Enum): - BOT = "Bot" - OAUTH2 = "Bearer" - - -class Authorization(ObjDict): - token_type: TokenType - token: str - - def to_authorization_header(self): - return {"Authorization": f"{self.token_type.value} {self.token}"} - - -Snowflake = str - -ISOTimestamp = type(str) # ISO8601 Timestamp - -Missing = type(None) - - -class Locale(Enum): - INDONESIAN = 'id' - DANISH = 'da' - GERMAN = 'de' - BRITISH_ENGLISH = 'en-GB' - AMERICAN_ENGLISH = 'en-US' - SPAIN_SPANISH = 'es-ES' - FRENCH = 'fr' - CROATIAN = 'hr' - ITALIAN = 'it' - LITHUANIAN = 'lt' - HUNGARIAN = 'hu' - DUTCH = 'nl' - NORWEGIAN = 'no' - POLISH = 'pl' - BRAZIL_PORTUGUESE = 'pt-BR' - ROMANIAN = 'ro' - FINNISH = 'fi' - SWEDISH = 'sv-SE' - VIETNAMESE = 'vi' - TURKISH = 'tr' - CZECH = 'cs' - GREEK = 'el' - BULGARIAN = 'bg' - RUSSIAN = 'ru' - UKRAINIAN = 'uk' - HINDI = 'hi' - THAI = 'th' - CHINESE = 'zh-CN' - JAPANESE = 'ja' - TAIWAN_CHINESE = 'zh-TW' - KOREAN = 'ko' - - def __str__(self) -> str: - return self.value diff --git a/dislord/discord/resources/__init__.py b/dislord/discord/resources/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/resources/application/__init__.py b/dislord/discord/resources/application/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/resources/application/enums.py b/dislord/discord/resources/application/enums.py deleted file mode 100644 index 920751dc..00000000 --- a/dislord/discord/resources/application/enums.py +++ /dev/null @@ -1,6 +0,0 @@ -from enum import IntEnum - - -class ApplicationIntegrationType(IntEnum): - GUILD_INSTALL = 0 - USER_INSTALL = 1 diff --git a/dislord/discord/resources/application/flags.py b/dislord/discord/resources/application/flags.py deleted file mode 100644 index 59a2b1da..00000000 --- a/dislord/discord/resources/application/flags.py +++ /dev/null @@ -1,14 +0,0 @@ -from enum import IntFlag - - -class ApplicationFlags(IntFlag): - APPLICATION_AUTO_MODERATION_RULE_CREATE_BADGE = 1 << 6 - GATEWAY_PRESENCE = 1 << 12 - GATEWAY_PRESENCE_LIMITED = 1 << 13 - GATEWAY_GUILD_MEMBERS = 1 << 14 - GATEWAY_GUILD_MEMBERS_LIMITED = 1 << 15 - VERIFICATION_PENDING_GUILD_LIMIT = 1 << 16 - EMBEDDED = 1 << 17 - GATEWAY_MESSAGE_CONTENT = 1 << 18 - GATEWAY_MESSAGE_CONTENT_LIMITED = 1 << 19 - APPLICATION_COMMAND_BADGE = 1 << 23 diff --git a/dislord/discord/resources/application/models.py b/dislord/discord/resources/application/models.py deleted file mode 100644 index 4a438b5e..00000000 --- a/dislord/discord/resources/application/models.py +++ /dev/null @@ -1,49 +0,0 @@ -from dislord.discord.resources.guild.guild import PartialGuild -from dislord.discord.resources.user.user import User -from dislord.types import ObjDict -from dislord.discord.resources.application.enums import ApplicationIntegrationType -from dislord.discord.resources.application.flags import ApplicationFlags -from dislord.discord.reference import Snowflake, Missing - - -class InstallParams(ObjDict): - scopes: list[str] - permissions: str - - -class ApplicationIntegrationTypeConfiguration(ObjDict): - oauth2_install_params: InstallParams | Missing - - -class PartialApplication(ObjDict): - id: Snowflake - name: str - icon: str | None - description: str - bot_public: bool - bot_require_code_grant: bool - summary: str # depreciated v11 - verify_key: str - # team: Team | None FIXME - - -class Application(PartialApplication): - rpc_origins: list[str] | Missing - bot: User | Missing - terms_of_service_url: str | Missing - privacy_policy_url: str | Missing - owner: User | Missing - guild_id: Snowflake | Missing - guild: PartialGuild | Missing - primary_sku_id: Snowflake | Missing - slug: str | Missing - cover_image: str | Missing - flags: ApplicationFlags | Missing - approximate_guild_count: int | Missing - redirect_uris: list[str] | Missing - interactions_endpoint_url: str | Missing - role_connections_verification_url: str | Missing - tags: list[str] | Missing - install_params: InstallParams | Missing - integration_types_config: dict[ApplicationIntegrationType, ApplicationIntegrationTypeConfiguration] | Missing - custom_install_url: str | Missing diff --git a/dislord/discord/resources/application_role_connection_metadata/__init__.py b/dislord/discord/resources/application_role_connection_metadata/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/resources/application_role_connection_metadata/enums.py b/dislord/discord/resources/application_role_connection_metadata/enums.py deleted file mode 100644 index 081fafe1..00000000 --- a/dislord/discord/resources/application_role_connection_metadata/enums.py +++ /dev/null @@ -1,12 +0,0 @@ -from enum import IntEnum - - -class ApplicationRoleConnectionMetadataType(IntEnum): - INTEGER_LESS_THAN_OR_EQUAL = 1 - INTEGER_GREATER_THAN_OR_EQUAL = 2 - INTEGER_EQUAL = 3 - INTEGER_NOT_EQUAL = 4 - DATETIME_LESS_THAN_OR_EQUAL = 5 - DATETIME_GREATER_THAN_OR_EQUAL = 6 - BOOLEAN_EQUAL = 7 - BOOLEAN_NOT_EQUAL = 8 \ No newline at end of file diff --git a/dislord/discord/resources/application_role_connection_metadata/models.py b/dislord/discord/resources/application_role_connection_metadata/models.py deleted file mode 100644 index 39b1ad0e..00000000 --- a/dislord/discord/resources/application_role_connection_metadata/models.py +++ /dev/null @@ -1,12 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.resources.application_role_connection_metadata.enums import ApplicationRoleConnectionMetadataType -from dislord.discord.reference import Missing, Locale - - -class ApplicationRoleConnectionMetadata(ObjDict): - type: ApplicationRoleConnectionMetadataType - key: str - name: str - name_localizations: dict[Locale, str] | Missing | None - description: str - description_localizations: dict[Locale, str] | Missing | None \ No newline at end of file diff --git a/dislord/discord/resources/audit_log/__init__.py b/dislord/discord/resources/audit_log/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/resources/audit_log/enums.py b/dislord/discord/resources/audit_log/enums.py deleted file mode 100644 index 0869b7ee..00000000 --- a/dislord/discord/resources/audit_log/enums.py +++ /dev/null @@ -1,69 +0,0 @@ -from enum import IntEnum - - -class AuditLogEvent(IntEnum): - GUILD_UPDATE = 1 - CHANNEL_CREATE = 10 - CHANNEL_UPDATE = 11 - CHANNEL_DELETE = 12 - CHANNEL_OVERWRITE_CREATE = 13 - CHANNEL_OVERWRITE_UPDATE = 14 - CHANNEL_OVERWRITE_DELETE = 15 - MEMBER_KICK = 20 - MEMBER_PRUNE = 21 - MEMBER_BAN_ADD = 22 - MEMBER_BAN_REMOVE = 23 - MEMBER_UPDATE = 24 - MEMBER_ROLE_UPDATE = 25 - MEMBER_MOVE = 26 - MEMBER_DISCONNECT = 27 - BOT_ADD = 28 - ROLE_CREATE = 30 - ROLE_UPDATE = 31 - ROLE_DELETE = 32 - INVITE_CREATE = 40 - INVITE_UPDATE = 41 - INVITE_DELETE = 42 - WEBHOOK_CREATE = 50 - WEBHOOK_UPDATE = 51 - WEBHOOK_DELETE = 52 - EMOJI_CREATE = 60 - EMOJI_UPDATE = 61 - EMOJI_DELETE = 62 - MESSAGE_DELETE = 72 - MESSAGE_BULK_DELETE = 73 - MESSAGE_PIN = 74 - MESSAGE_UNPIN = 75 - INTEGRATION_CREATE = 80 - INTEGRATION_UPDATE = 81 - INTEGRATION_DELETE = 82 - STAGE_INSTANCE_CREATE = 83 - STAGE_INSTANCE_UPDATE = 84 - STAGE_INSTANCE_DELETE = 85 - STICKER_CREATE = 90 - STICKER_UPDATE = 91 - STICKER_DELETE = 92 - GUILD_SCHEDULED_EVENT_CREATE = 100 - GUILD_SCHEDULED_EVENT_UPDATE = 101 - GUILD_SCHEDULED_EVENT_DELETE = 102 - THREAD_CREATE = 110 - THREAD_UPDATE = 111 - THREAD_DELETE = 112 - APPLICATION_COMMAND_PERMISSION_UPDATE = 121 - AUTO_MODERATION_RULE_CREATE = 140 - AUTO_MODERATION_RULE_UPDATE = 141 - AUTO_MODERATION_RULE_DELETE = 142 - AUTO_MODERATION_BLOCK_MESSAGE = 143 - AUTO_MODERATION_FLAG_TO_CHANNEL = 144 - AUTO_MODERATION_USER_COMMUNICATION_DISABLED = 145 - CREATOR_MONETIZATION_REQUEST_CREATED = 150 - CREATOR_MONETIZATION_TERMS_ACCEPTED = 151 - ONBOARDING_PROMPT_CREATE = 163 - ONBOARDING_PROMPT_UPDATE = 164 - ONBOARDING_PROMPT_DELETE = 165 - ONBOARDING_CREATE = 166 - ONBOARDING_UPDATE = 167 - HOME_SETTINGS_CREATE = 190 - HOME_SETTINGS_UPDATE = 191 - - diff --git a/dislord/discord/resources/audit_log/models.py b/dislord/discord/resources/audit_log/models.py deleted file mode 100644 index 6debcebb..00000000 --- a/dislord/discord/resources/audit_log/models.py +++ /dev/null @@ -1,48 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.interactions.application_commands.models import ApplicationCommand -from dislord.discord.resources.audit_log.enums import AuditLogEvent -from dislord.discord.resources.auto_moderation.models import AutoModerationRule -from dislord.discord.resources.channel.channel import Channel -from dislord.discord.reference import Snowflake, Missing - - -class AuditLogChange(ObjDict): - new_value: str | int | float | Missing - old_value: str | int | float | Missing - key: str - - -class OptionalAuditEntryInfo(ObjDict): - application_id: Snowflake - auto_moderation_rule_name: str - auto_moderation_rule_trigger_type: str - channel_id: Snowflake - count: str - delete_member_days: str - id: Snowflake - members_removed: str - message_id: Snowflake - role_name: str - type: str - integration_type: str - - -class AuditLogEntry(ObjDict): - target_id: str | None - changes: list[AuditLogChange] | Missing - user_id: Snowflake | None - id: Snowflake - action_type: AuditLogEvent - options: OptionalAuditEntryInfo | Missing - reason: str | Missing - - -class AuditLog(ObjDict): - application_commands: list[ApplicationCommand] - audit_log_entries: list[AuditLogEntry] - auto_moderation_rules: list[AutoModerationRule] - # guild_scheduled_events: list[GuildScheduledEvent] FIXME - # integrations: list[PartialIntegration] FIXME - threads: list[Channel] - # users: list[User] FIXME - # webhooks: list[Webhook] FIXME diff --git a/dislord/discord/resources/auto_moderation/__init__.py b/dislord/discord/resources/auto_moderation/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/resources/auto_moderation/enums.py b/dislord/discord/resources/auto_moderation/enums.py deleted file mode 100644 index 3deee192..00000000 --- a/dislord/discord/resources/auto_moderation/enums.py +++ /dev/null @@ -1,24 +0,0 @@ -from enum import IntEnum - - -class TriggerType(IntEnum): - KEYWORD = 1 - SPAM = 3 - KEYWORD_PRESET = 4 - MENTION_SPAM = 5 - - -class KeywordPresetType(IntEnum): - PROFANITY = 1 - SEXUAL_CONTENT = 2 - SLURS = 3 - - -class EventType(IntEnum): - MESSAGE_SEND = 1 - - -class ActionType(IntEnum): - BLOCK_MESSAGE = 1 - SEND_ALERT_MESSAGE = 2 - TIMEOUT = 3 diff --git a/dislord/discord/resources/auto_moderation/models.py b/dislord/discord/resources/auto_moderation/models.py deleted file mode 100644 index 05131338..00000000 --- a/dislord/discord/resources/auto_moderation/models.py +++ /dev/null @@ -1,36 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.resources.auto_moderation.enums import TriggerType, EventType, KeywordPresetType, ActionType -from dislord.discord.reference import Snowflake, Missing - -class ActionMetadata(ObjDict): - channel_id: Snowflake - duration_seconds: int - custom_message: str | Missing - - -class AutoModerationAction(ObjDict): - type: ActionType - metadata: ActionMetadata | Missing - - -class TriggerMetadata(ObjDict): - keyword_filter: list[str] - regex_patterns: list[str] - presets: list[KeywordPresetType] - allow_list: list[str] - mention_total_limit: int - mention_raid_protection_enabled: bool - - -class AutoModerationRule(ObjDict): - id: Snowflake - guild_id: Snowflake - name: str - creator_id: Snowflake - event_type: EventType - trigger_type: TriggerType - trigger_metadata: TriggerMetadata - actions: list[AutoModerationAction] - enabled: bool - exempt_roles: list[Snowflake] - exempt_channels: list[Snowflake] \ No newline at end of file diff --git a/dislord/discord/resources/channel/__init__.py b/dislord/discord/resources/channel/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/resources/channel/allowed_mentions.py b/dislord/discord/resources/channel/allowed_mentions.py deleted file mode 100644 index 20e2351c..00000000 --- a/dislord/discord/resources/channel/allowed_mentions.py +++ /dev/null @@ -1,17 +0,0 @@ -from enum import StrEnum - -from dislord.types import ObjDict -from dislord.discord.reference import Snowflake - - -class AllowedMentionType(StrEnum): - ROLES = "roles" - USERS = "users" - EVERYONE = "everyone" - - -class AllowedMentions(ObjDict): - parse: list[AllowedMentionType] - role: list[Snowflake] - users: list[Snowflake] - replied_user: bool diff --git a/dislord/discord/resources/channel/attachment.py b/dislord/discord/resources/channel/attachment.py deleted file mode 100644 index b99ee464..00000000 --- a/dislord/discord/resources/channel/attachment.py +++ /dev/null @@ -1,27 +0,0 @@ -from enum import IntFlag - -from dislord.types import ObjDict -from dislord.discord.reference import Snowflake, Missing - - -class AttachmentFlag(IntFlag): - IS_REMIX = 1 << 2 - - -class PartialAttachment(ObjDict): - id: Snowflake - filename: str - description: str | Missing - - -class Attachment(PartialAttachment): - content_type: str | Missing - size: int - url: str - proxy_url: str - height: int | Missing | None - width: int | Missing | None - ephemeral: bool | Missing - duration_secs: float | Missing - waveform: str | Missing - flags: AttachmentFlag diff --git a/dislord/discord/resources/channel/channel.py b/dislord/discord/resources/channel/channel.py deleted file mode 100644 index db9e8fc9..00000000 --- a/dislord/discord/resources/channel/channel.py +++ /dev/null @@ -1,89 +0,0 @@ -from enum import IntFlag, IntEnum - -from dislord.types import ObjDict -from dislord.discord.resources.channel.default_reaction import DefaultReaction -from dislord.discord.resources.channel.forum_tag import ForumTag -from dislord.discord.resources.channel.overwrite import Overwrite -from dislord.discord.resources.channel.thread_member import ThreadMember -from dislord.discord.resources.channel.thread_metadata import ThreadMetadata -from dislord.discord.resources.user.user import User -from dislord.discord.reference import Missing, Snowflake, ISOTimestamp - - -class ForumLayoutType(IntEnum): - NOT_SET = 0 - LIST_VIEW = 1 - GALLERY_VIEW = 2 - - -class SortOrderType(IntEnum): - LATEST_ACTIVITY = 0 - CREATION_DATE = 1 - - -class ChannelFlags(IntFlag): - PINNED = 1 << 1 - REQUIRE_TAG = 1 << 4 - HIDE_MEDIA_DOWNLOAD_OPTIONS = 1 << 15 - - -class VideoQualityMode(IntEnum): - AUTO = 1 - FULL = 2 - - -class ChannelType(IntEnum): - GUILD_TEXT = 0 - DM = 1 - GUILD_VOICE = 2 - GROUP_DM = 3 - GUILD_CATEGORY = 4 - GUILD_ANNOUNCEMENT = 5 - ANNOUNCEMENT_THREAD = 10 - PUBLIC_THREAD = 11 - PRIVATE_THREAD = 12 - GUILD_STAGE_VOICE = 13 - GUILD_DIRECTORY = 14 - GUILD_FORUM = 15 - GUILD_MEDIA = 16 - - -class PartialChannel(ObjDict): - id: Snowflake - type: ChannelType - name: str | Missing | None - permissions: str | Missing - thread_metadata: ThreadMetadata | Missing - parent_id: Snowflake | Missing | None - - -class Channel(PartialChannel): - guild_id: Snowflake | Missing - position: int | Missing - permission_overwrites: list[Overwrite] | Missing - topic: str | Missing | None - nsfw: bool | Missing - last_message_id: Snowflake | Missing | None - bitrate: int | Missing - user_limit: int | Missing - rate_limit_per_user: int | Missing - recipients: list[User] | Missing - icon: str | Missing | None - owner_id: Snowflake | Missing - application_id: Snowflake | Missing - managed: bool | Missing - last_pin_timestamp: ISOTimestamp | Missing | None - rtc_region: str | Missing | None - video_quality_mode: int | Missing - message_count: int | Missing - member_count: int | Missing - member: ThreadMember | Missing - default_auto_archive_duration: int | Missing - flags: ChannelFlags | Missing - total_message_sent: int | Missing - available_tags: list[ForumTag] | Missing - applied_tags: list[Snowflake] | Missing - default_reaction_emoji: DefaultReaction | Missing | None - default_thread_rate_limit_per_user: int | Missing - default_sort_order: int | Missing | None - default_forum_layout: int | Missing diff --git a/dislord/discord/resources/channel/channel_mention.py b/dislord/discord/resources/channel/channel_mention.py deleted file mode 100644 index efed9445..00000000 --- a/dislord/discord/resources/channel/channel_mention.py +++ /dev/null @@ -1,10 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.resources.channel.channel import ChannelType -from dislord.discord.reference import Snowflake - - -class ChannelMention(ObjDict): - id: Snowflake - guild_id: Snowflake - type: ChannelType - name: str diff --git a/dislord/discord/resources/channel/default_reaction.py b/dislord/discord/resources/channel/default_reaction.py deleted file mode 100644 index 6f883880..00000000 --- a/dislord/discord/resources/channel/default_reaction.py +++ /dev/null @@ -1,8 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.reference import Snowflake -from dislord.types import ObjDict - - -class DefaultReaction(ObjDict): - emoji_id: Snowflake | None - emoji_name: str | None diff --git a/dislord/discord/resources/channel/embed.py b/dislord/discord/resources/channel/embed.py deleted file mode 100644 index fdf641f4..00000000 --- a/dislord/discord/resources/channel/embed.py +++ /dev/null @@ -1,63 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.reference import Missing, ISOTimestamp - - -class EmbedField(ObjDict): - name: str - value: str - inline: bool | Missing - - -class EmbedFooter(ObjDict): - text: str - icon_url: str | Missing - proxy_icon_url: str | Missing - - -class EmbedAuthor(ObjDict): - name: str - url: str | Missing - icon_url: str | Missing - proxy_icon_url: str | Missing - - -class EmbedProvider(ObjDict): - name: str | Missing - url: str | Missing - - -class EmbedImage(ObjDict): - url: str - proxy_url: str | Missing - height: int | Missing - width: int | Missing - - -class EmbedVideo(ObjDict): - url: str | Missing - proxy_url: str | Missing - height: int | Missing - width: int | Missing - - -class EmbedThumbnail(ObjDict): - url: str - proxy_url: str | Missing - height: int | Missing - width: int | Missing - - -class Embed(ObjDict): - title: str | Missing - type: str | Missing - description: str | Missing - url: str | Missing - timestamp: ISOTimestamp | Missing - color: int | Missing - footer: EmbedFooter | Missing - image: EmbedImage | Missing - thumbnail: EmbedThumbnail | Missing - video: EmbedVideo | Missing - provider: EmbedProvider | Missing - author: EmbedAuthor | Missing - fields: list[EmbedField] | Missing diff --git a/dislord/discord/resources/channel/enums.py b/dislord/discord/resources/channel/enums.py deleted file mode 100644 index f49acb15..00000000 --- a/dislord/discord/resources/channel/enums.py +++ /dev/null @@ -1,5 +0,0 @@ -from enum import IntEnum, StrEnum - - - - diff --git a/dislord/discord/resources/channel/followed_channel.py b/dislord/discord/resources/channel/followed_channel.py deleted file mode 100644 index f3650a98..00000000 --- a/dislord/discord/resources/channel/followed_channel.py +++ /dev/null @@ -1,7 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.reference import Snowflake - - -class FollowedChannel(ObjDict): - channel_id: Snowflake - webhook_id: Snowflake diff --git a/dislord/discord/resources/channel/forum_tag.py b/dislord/discord/resources/channel/forum_tag.py deleted file mode 100644 index 33550752..00000000 --- a/dislord/discord/resources/channel/forum_tag.py +++ /dev/null @@ -1,10 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.reference import Snowflake - - -class ForumTag(ObjDict): - id: Snowflake - name: str - moderated: bool - emoji_id: Snowflake | None - emoji_name: str | None diff --git a/dislord/discord/resources/channel/message.py b/dislord/discord/resources/channel/message.py deleted file mode 100644 index f03fdf47..00000000 --- a/dislord/discord/resources/channel/message.py +++ /dev/null @@ -1,66 +0,0 @@ -from enum import IntFlag, IntEnum -from typing import Self - -from dislord.types import ObjDict -from dislord.discord.interactions.components.models import Component -from dislord.discord.interactions.receiving_and_responding.interaction_data import ResolvedData -from dislord.discord.interactions.receiving_and_responding.message_interaction import MessageInteraction -from dislord.discord.resources.application.models import PartialApplication -from dislord.discord.resources.channel.channel import Channel -from dislord.discord.resources.channel.channel_mention import ChannelMention -from dislord.discord.resources.channel.message_interaction_metadata import MessageInteractionMetadata -from dislord.discord.resources.channel.message_reference import MessageReference -from dislord.discord.resources.channel.partial_message import PartialMessage -from dislord.discord.resources.channel.role_subscription_data import RoleSubscriptionData -from dislord.discord.resources.poll.poll import Poll -from dislord.discord.resources.sticker.sticker import Sticker -from dislord.discord.resources.sticker.sticker_item import StickerItem -from dislord.discord.reference import Missing, Snowflake - - -class MessageFlags(IntFlag): - CROSSPOSTED = 1 << 0 - IS_CROSSPOST = 1 << 1 - SUPPRESS_EMBEDS = 1 << 2 - SOURCE_MESSAGE_DELETED = 1 << 3 - URGENT = 1 << 4 - HAS_THREAD = 1 << 5 - EPHEMERAL = 1 << 6 - LOADING = 1 << 7 - FAILED_TO_MENTION_SOME_ROLES_IN_THREAD = 1 << 8 - SUPPRESS_NOTIFICATIONS = 1 << 12 - IS_VOICE_MESSAGE = 1 << 13 - - -class MessageActivityType(IntEnum): - JOIN = 1 - SPECTATE = 2 - LISTEN = 3 - JOIN_REQUEST = 5 - - -class MessageActivity(ObjDict): - type: MessageActivityType - party_id: str | Missing - - -class Message(PartialMessage): - mention_channels: list[ChannelMention] | Missing - nonce: int | str | Missing - webhook_id: Snowflake | Missing - activity: MessageActivity | Missing - application: PartialApplication | Missing - application_id: Snowflake | Missing - message_reference: MessageReference | Missing - flags: MessageFlags | Missing - referenced_message: Self | Missing | None - interaction_metadata: MessageInteractionMetadata | Missing - interaction: MessageInteraction | Missing - thread: Channel | Missing - components: list[Component] | Missing - sticker_items: list[StickerItem] | Missing - stickers: list[Sticker] | Missing - position: int | Missing - role_subscription_data = RoleSubscriptionData | Missing - resolved: ResolvedData | Missing - poll: Poll | Missing diff --git a/dislord/discord/resources/channel/message_call.py b/dislord/discord/resources/channel/message_call.py deleted file mode 100644 index fed37ddb..00000000 --- a/dislord/discord/resources/channel/message_call.py +++ /dev/null @@ -1,7 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.reference import Snowflake, ISOTimestamp, Missing - - -class MessageCall(ObjDict): - participants: list[Snowflake] - ended_timestamp: ISOTimestamp | Missing | None diff --git a/dislord/discord/resources/channel/message_interaction_metadata.py b/dislord/discord/resources/channel/message_interaction_metadata.py deleted file mode 100644 index 7113ddfb..00000000 --- a/dislord/discord/resources/channel/message_interaction_metadata.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Self - -from dislord.types import ObjDict -from dislord.discord.interactions.receiving_and_responding.message_interaction import InteractionType -from dislord.discord.resources.application.enums import ApplicationIntegrationType -from dislord.discord.resources.user.user import User -from dislord.discord.reference import Snowflake, Missing - - -class MessageInteractionMetadata(ObjDict): - id: Snowflake - type: InteractionType - user: User - authorizing_integration_owners: dict[ApplicationIntegrationType, Snowflake] - original_response_message_id: Snowflake | Missing - interacted_message_id: Snowflake | Missing - triggering_interaction_metadata: Self | Missing diff --git a/dislord/discord/resources/channel/message_reference.py b/dislord/discord/resources/channel/message_reference.py deleted file mode 100644 index cf19468b..00000000 --- a/dislord/discord/resources/channel/message_reference.py +++ /dev/null @@ -1,9 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.reference import Snowflake, Missing - - -class MessageReference(ObjDict): - message_id: Snowflake | Missing - channel_id: Snowflake | Missing - guild_id: Snowflake | Missing - fail_if_not_exists: bool | Missing diff --git a/dislord/discord/resources/channel/overwrite.py b/dislord/discord/resources/channel/overwrite.py deleted file mode 100644 index 90908e79..00000000 --- a/dislord/discord/resources/channel/overwrite.py +++ /dev/null @@ -1,16 +0,0 @@ -from enum import IntEnum - -from dislord.types import ObjDict -from dislord.discord.reference import Snowflake - - -class OverwriteType(IntEnum): - ROLE = 0 - MEMBER = 1 - - -class Overwrite(ObjDict): - id: Snowflake - type: OverwriteType - allow: str - deny: str diff --git a/dislord/discord/resources/channel/partial_message.py b/dislord/discord/resources/channel/partial_message.py deleted file mode 100644 index 8546f1e5..00000000 --- a/dislord/discord/resources/channel/partial_message.py +++ /dev/null @@ -1,61 +0,0 @@ -from enum import IntEnum - -from dislord.types import ObjDict -from dislord.discord.topics.permissions.role import Role -from dislord.discord.resources.channel.attachment import Attachment -from dislord.discord.resources.channel.embed import Embed -from dislord.discord.resources.channel.reaction import Reaction -from dislord.discord.resources.user.user import User -from dislord.discord.reference import Snowflake, ISOTimestamp - - -class MessageType(IntEnum): - DEFAULT = 0 - RECIPIENT_ADD = 1 - RECIPIENT_REMOVE = 2 - CALL = 3 - CHANNEL_NAME_CHANGE = 4 - CHANNEL_ICON_CHANGE = 5 - CHANNEL_PINNED_MESSAGE = 6 - USER_JOIN = 7 - GUILD_BOOST = 8 - GUILD_BOOST_TIER_1 = 9 - GUILD_BOOST_TIER_2 = 10 - GUILD_BOOST_TIER_3 = 11 - CHANNEL_FOLLOW_ADD = 12 - GUILD_DISCOVERY_DISQUALIFIED = 14 - GUILD_DISCOVERY_REQUALIFIED = 15 - GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING = 16 - GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING = 17 - THREAD_CREATED = 18 - REPLY = 19 - CHAT_INPUT_COMMAND = 20 - THREAD_STARTER_MESSAGE = 21 - GUILD_INVITE_REMINDER = 22 - CONTEXT_MENU_COMMAND = 23 - AUTO_MODERATION_ACTION = 24 - ROLE_SUBSCRIPTION_PURCHASE = 25 - INTERACTION_PREMIUM_UPSELL = 26 - STAGE_START = 27 - STAGE_END = 28 - STAGE_SPEAKER = 29 - STAGE_TOPIC = 31 - GUILD_APPLICATION_PREMIUM_SUBSCRIPTION = 32 - - -class PartialMessage(ObjDict): - id: Snowflake - channel_id: Snowflake - author: User - content: str - timestamp: ISOTimestamp - edited_timestamp: ISOTimestamp | None - tts: bool - mention_everyone: bool - mentions: list[User] - mention_roles: list[Role] - attachments: list[Attachment] - embeds: list[Embed] - reactions: list[Reaction] - pinned: bool - type: MessageType diff --git a/dislord/discord/resources/channel/reaction.py b/dislord/discord/resources/channel/reaction.py deleted file mode 100644 index 2236ee36..00000000 --- a/dislord/discord/resources/channel/reaction.py +++ /dev/null @@ -1,13 +0,0 @@ -from dislord.types import ObjDict -from dislord.model.base import BaseModel, HexColor -from dislord.discord.resources.channel.reaction_count_details import ReactionCountDetails -from dislord.discord.resources.emoji.emoji import PartialEmoji - - -class Reaction(ObjDict): - count: int - count_details: ReactionCountDetails - me: bool - me_burst: bool - emoji: PartialEmoji - bust_colors: list[HexColor] diff --git a/dislord/discord/resources/channel/reaction_count_details.py b/dislord/discord/resources/channel/reaction_count_details.py deleted file mode 100644 index 71810833..00000000 --- a/dislord/discord/resources/channel/reaction_count_details.py +++ /dev/null @@ -1,6 +0,0 @@ -from dislord.types import ObjDict - - -class ReactionCountDetails(ObjDict): - burst: int - normal: int diff --git a/dislord/discord/resources/channel/role_subscription_data.py b/dislord/discord/resources/channel/role_subscription_data.py deleted file mode 100644 index 8d5ab6d4..00000000 --- a/dislord/discord/resources/channel/role_subscription_data.py +++ /dev/null @@ -1,10 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.reference import Snowflake - - -class RoleSubscriptionData(ObjDict): - role_subscription_listing_id = Snowflake - tier_name: str - total_months_subscribed = int - is_renewal = bool - diff --git a/dislord/discord/resources/channel/thread_member.py b/dislord/discord/resources/channel/thread_member.py deleted file mode 100644 index 1ce32969..00000000 --- a/dislord/discord/resources/channel/thread_member.py +++ /dev/null @@ -1,13 +0,0 @@ -from enum import IntFlag - -from dislord.types import ObjDict -from dislord.discord.resources.guild.guild_member import GuildMember -from dislord.discord.reference import Snowflake, Missing, ISOTimestamp - - -class ThreadMember(ObjDict): - id: Snowflake | Missing - user_id: Snowflake | Missing - join_timestamp: ISOTimestamp - flags: IntFlag # Any user-thread settings, currently only used for notifications - member: GuildMember | Missing diff --git a/dislord/discord/resources/channel/thread_metadata.py b/dislord/discord/resources/channel/thread_metadata.py deleted file mode 100644 index 19b64414..00000000 --- a/dislord/discord/resources/channel/thread_metadata.py +++ /dev/null @@ -1,11 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.reference import ISOTimestamp, Missing - - -class ThreadMetadata(ObjDict): - archived: bool - auto_archive_duration: int - archive_timestamp: ISOTimestamp - locked: bool - invitable: bool | Missing - create_timestamp: ISOTimestamp | Missing | None diff --git a/dislord/discord/resources/emoji/__init__.py b/dislord/discord/resources/emoji/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/resources/emoji/emoji.py b/dislord/discord/resources/emoji/emoji.py deleted file mode 100644 index 53ef9c8d..00000000 --- a/dislord/discord/resources/emoji/emoji.py +++ /dev/null @@ -1,17 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.resources.user.user import User -from dislord.discord.reference import Snowflake, Missing - - -class PartialEmoji(ObjDict): - id: Snowflake | None - name: str | None - animated: bool | Missing - - -class Emoji(PartialEmoji): - roles: list[Snowflake] | Missing - user: User - require_colons: bool | Missing - managed: bool | Missing - available: bool | Missing diff --git a/dislord/discord/resources/guild/__init__.py b/dislord/discord/resources/guild/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/resources/guild/ban.py b/dislord/discord/resources/guild/ban.py deleted file mode 100644 index 0c190348..00000000 --- a/dislord/discord/resources/guild/ban.py +++ /dev/null @@ -1,8 +0,0 @@ - -from dislord.types import ObjDict -from dislord.discord.resources.user.user import User - - -class Ban(ObjDict): - reason: str | None - user: User diff --git a/dislord/discord/resources/guild/guild.py b/dislord/discord/resources/guild/guild.py deleted file mode 100644 index c693b1cc..00000000 --- a/dislord/discord/resources/guild/guild.py +++ /dev/null @@ -1,141 +0,0 @@ -from enum import IntEnum, IntFlag, Flag - -from dislord.types import ObjDict -from dislord.discord.topics.permissions.role import Role -from dislord.discord.resources.emoji.emoji import Emoji -from dislord.discord.resources.guild.welcome_screen import WelcomeScreen -from dislord.discord.resources.sticker.sticker import Sticker -from dislord.discord.reference import Missing, Snowflake, Locale - - -class MutableGuildFeatures(Flag): - COMMUNITY = "COMMUNITY" - DISCOVERABLE = "DISCOVERABLE" - INVITES_DISABLED = "INVITES_DISABLED" - RAID_ALERTS_DISABLED = "RAID_ALERTS_DISABLED" - - -class GuildFeatures(Flag): - ANIMATED_BANNER = "ANIMATED_BANNER" - ANIMATED_ICON = "ANIMATED_ICON" - APPLICATION_COMMAND_PERMISSIONS_V2 = "APPLICATION_COMMAND_PERMISSIONS_V2" - AUTO_MODERATION = "AUTO_MODERATION" - BANNER = "BANNER" - COMMUNITY = "COMMUNITY" - CREATOR_MONETIZABLE_PROVISIONAL = "CREATOR_MONETIZABLE_PROVISIONAL" - CREATOR_STORE_PAGE = "CREATOR_STORE_PAGE" - DEVELOPER_SUPPORT_SERVER = "DEVELOPER_SUPPORT_SERVER" - DISCOVERABLE = "DISCOVERABLE" - FEATURABLE = "FEATURABLE" - INVITES_DISABLED = "INVITES_DISABLED" - INVITE_SPLASH = "INVITE_SPLASH" - MEMBER_VERIFICATION_GATE_ENABLED = "MEMBER_VERIFICATION_GATE_ENABLED" - MORE_STICKERS = "MORE_STICKERS" - NEWS = "NEWS" - PARTNERED = "PARTNERED" - PREVIEW_ENABLED = "PREVIEW_ENABLED" - RAID_ALERTS_DISABLED = "RAID_ALERTS_DISABLED" - ROLE_ICONS = "ROLE_ICONS" - ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE = "ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE" - ROLE_SUBSCRIPTIONS_ENABLED = "ROLE_SUBSCRIPTIONS_ENABLED" - TICKETED_EVENTS_ENABLED = "TICKETED_EVENTS_ENABLED" - VANITY_URL = "VANITY_URL" - VERIFIED = "VERIFIED" - VIP_REGIONS = "VIP_REGIONS" - WELCOME_SCREEN_ENABLED = "WELCOME_SCREEN_ENABLED" - - -class SystemChannelFlags(IntFlag): - SUPPRESS_JOIN_NOTIFICATIONS = 1 << 0 - SUPPRESS_PREMIUM_SUBSCRIPTIONS = 1 << 1 - SUPPRESS_GUILD_REMINDER_NOTIFICATIONS = 1 << 2 - SUPPRESS_JOIN_NOTIFICATION_REPLIES = 1 << 3 - SUPPRESS_ROLE_SUBSCRIPTION_PURCHASE_NOTIFICATIONS = 1 << 4 - SUPPRESS_ROLE_SUBSCRIPTION_PURCHASE_NOTIFICATION_REPLIES = 1 << 5 - - -class PremiumTier(IntEnum): - NONE = 0 - TIER_1 = 1 - TIER_2 = 2 - TIER_3 = 3 - - -class GuildNsfwLevel(IntEnum): - DEFAULT = 0 - EXPLICIT = 1 - SAFE = 2 - AGE_RESTRICTED = 3 - - -class VerificationLevel(IntEnum): - NONE = 0 - LOW = 1 - MEDIUM = 2 - HIGH = 3 - VERY_HIGH = 4 - - -class MfaLevel(IntEnum): - NONE = 0 - ELEVATED = 1 - - -class ExplicitContentFilterLevel(IntEnum): - DISABLED = 0 - MEMBERS_WITHOUT_ROLES = 1 - ALL_MEMBERS = 2 - - -class DefaultMessageNotificationLevel(IntEnum): - ALL_MESSAGES = 0 - ONLY_MENTIONS = 1 - - -class PartialGuild(ObjDict): - id: Snowflake - name: str - description: str | None - region: str | Missing - afk_channel_id: Snowflake | None - system_channel_id: Snowflake | None - icon_hash: str | Missing | None - - -class Guild(PartialGuild): - icon: str | None - splash: str | None - discovery_splash: str | None - owner: bool | Missing - owner_id: Snowflake - permissions: str | Missing - afk_timeout: int - widget_enabled: bool | Missing - widget_channel_id: Snowflake | Missing | None - verification_level: VerificationLevel - default_message_notifications: DefaultMessageNotificationLevel - explicit_content_filter: ExplicitContentFilterLevel - roles: list[Role] - emojis: list[Emoji] - features: list[GuildFeatures] - mfa_level: MfaLevel - application_id: Snowflake | None - system_channel_flags: SystemChannelFlags - rules_channel_id: Snowflake | None - max_presences: int | Missing | None - max_members: int | Missing - vanity_url_code: str | None - banner: str | None - premium_tier: PremiumTier - premium_subscription_count: int | Missing - preferred_locale: Locale - public_updates_channel_id: Snowflake | None - max_video_channel_users: int | Missing - max_stage_video_channel_users: int | Missing - approximate_member_count: int | Missing - approximate_presence_count: int | Missing - welcome_screen: WelcomeScreen | Missing - nsfw_level: GuildNsfwLevel - stickers: list[Sticker] | Missing - premium_progress_bar_enabled: bool - safety_alerts_channel_id: Snowflake | None diff --git a/dislord/discord/resources/guild/guild_member.py b/dislord/discord/resources/guild/guild_member.py deleted file mode 100644 index 356d8fa0..00000000 --- a/dislord/discord/resources/guild/guild_member.py +++ /dev/null @@ -1,32 +0,0 @@ -from enum import IntFlag - -from dislord.types import ObjDict -from dislord.discord.resources.user.avatar_decoration_data import AvatarDecorationData -from dislord.discord.resources.user.user import User -from dislord.discord.reference import Missing, Snowflake, ISOTimestamp - - -class GuildMemberFlags(IntFlag): - DID_REJOIN = 1 << 0 - COMPLETED_ONBOARDING = 1 << 1 - BYPASSES_VERIFICATION = 1 << 2 - STARTED_ONBOARDING = 1 << 3 - - -class PartialGuildMember(ObjDict): - nick: str | Missing | None - avatar: str | Missing | None - roles: list[Snowflake] - joined_at: ISOTimestamp - premium_since: ISOTimestamp | Missing | None - flags: GuildMemberFlags - pending: bool | Missing - permissions: str | Missing - communication_disabled_until: ISOTimestamp | Missing | None - avatar_decoration_data: AvatarDecorationData | Missing | None - - -class GuildMember(PartialGuildMember): - user: User | Missing - deaf: bool - mute: bool diff --git a/dislord/discord/resources/guild/guild_onboarding.py b/dislord/discord/resources/guild/guild_onboarding.py deleted file mode 100644 index 1a996acc..00000000 --- a/dislord/discord/resources/guild/guild_onboarding.py +++ /dev/null @@ -1,45 +0,0 @@ -from enum import IntFlag - -from dislord.types import ObjDict -from dislord.discord.resources.emoji.emoji import Emoji -from dislord.discord.reference import Snowflake, Missing - - -class PromptType(IntFlag): - MULTIPLE_CHOICE = 0 - DROPDOWN = 1 - - -class OnboardingMode(IntFlag): - ONBOARDING_DEFAULT = 0 - ONBOARDING_ADVANCED = 1 - - -class PromptOption(ObjDict): - id: Snowflake - channel_ids: list[Snowflake] - role_ids: list[Snowflake] - emoji: Emoji | Missing - emoji_id: Snowflake | Missing - emoji_name: str | Missing - emoji_animated: bool | Missing - title: str - description: str - - -class OnboardingPrompt(Snowflake): - id: Snowflake - type: PromptType - options: list[PromptOption] - title: str - single_select: bool - required: bool - in_onboarding: bool - - -class GuildOnboarding(ObjDict): - guild_id: Snowflake - prompts: list[OnboardingPrompt] - default_channel_ids: list[Snowflake] - enabled: bool - mode: OnboardingMode diff --git a/dislord/discord/resources/guild/guild_preview.py b/dislord/discord/resources/guild/guild_preview.py deleted file mode 100644 index 80c2dc95..00000000 --- a/dislord/discord/resources/guild/guild_preview.py +++ /dev/null @@ -1,19 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.resources.emoji.emoji import Emoji -from dislord.discord.resources.guild.guild import GuildFeatures -from dislord.discord.resources.sticker.sticker import Sticker -from dislord.discord.reference import Snowflake - - -class GuildPreview(ObjDict): - id: Snowflake - name: str - icon: str | None - splash: str | None - discovery_splash: str | None - emojis: list[Emoji] - features: list[GuildFeatures] - approximate_member_count: int - approximate_presence_count: int - description: str | None - stickers: list[Sticker] diff --git a/dislord/discord/resources/guild/guild_widget.py b/dislord/discord/resources/guild/guild_widget.py deleted file mode 100644 index 3a989a27..00000000 --- a/dislord/discord/resources/guild/guild_widget.py +++ /dev/null @@ -1,13 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.resources.channel.channel import PartialChannel -from dislord.discord.resources.user.user import PartialUser -from dislord.discord.reference import Snowflake - - -class GuildWidget(ObjDict): - id: Snowflake - name: str - instant_invite: str | None - channels: list[PartialChannel] - members: list[PartialUser] - presence_count: int diff --git a/dislord/discord/resources/guild/guild_widget_settings.py b/dislord/discord/resources/guild/guild_widget_settings.py deleted file mode 100644 index 3073f2bb..00000000 --- a/dislord/discord/resources/guild/guild_widget_settings.py +++ /dev/null @@ -1,7 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.reference import Snowflake - - -class GuildWidgetSettings(ObjDict): - enabled: bool - channel_id: Snowflake | None diff --git a/dislord/discord/resources/guild/integration.py b/dislord/discord/resources/guild/integration.py deleted file mode 100644 index 7eda7c1f..00000000 --- a/dislord/discord/resources/guild/integration.py +++ /dev/null @@ -1,31 +0,0 @@ -from enum import IntEnum - -from dislord.types import ObjDict -from dislord.discord.resources.guild.integration_account import IntegrationAccount -from dislord.discord.resources.guild.integration_application import IntegrationApplication -from dislord.discord.resources.user.user import User -from dislord.discord.reference import Snowflake, Missing, ISOTimestamp - - -class IntegrationExpireBehavior(IntEnum): - REMOVE_ROLE = 0 - KICK = 1 - - -class Integration(ObjDict): - id: Snowflake - name: str - type: str - enabled: bool - syncing: bool | Missing - role_id: Snowflake | Missing - enable_emoticons: bool | Missing - expire_behavior: IntegrationExpireBehavior | Missing - expire_grace_period: int | Missing - user: User | Missing - account: IntegrationAccount - synced_at: ISOTimestamp | Missing - subscriber_count: int | Missing - revoked: bool | Missing - application: IntegrationApplication | Missing - # scopes: list[Oauth2Scopes] | Missing FIXME diff --git a/dislord/discord/resources/guild/integration_account.py b/dislord/discord/resources/guild/integration_account.py deleted file mode 100644 index 9eecac7d..00000000 --- a/dislord/discord/resources/guild/integration_account.py +++ /dev/null @@ -1,6 +0,0 @@ -from dislord.types import ObjDict - - -class IntegrationAccount(ObjDict): - id: str - name: str diff --git a/dislord/discord/resources/guild/integration_application.py b/dislord/discord/resources/guild/integration_application.py deleted file mode 100644 index c906c2be..00000000 --- a/dislord/discord/resources/guild/integration_application.py +++ /dev/null @@ -1,11 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.resources.user.user import User -from dislord.discord.reference import Snowflake, Missing - - -class IntegrationApplication(ObjDict): - id: Snowflake - name: str - icon: str | None - description: str - bot: User | Missing diff --git a/dislord/discord/resources/guild/welcome_screen.py b/dislord/discord/resources/guild/welcome_screen.py deleted file mode 100644 index c05b0e91..00000000 --- a/dislord/discord/resources/guild/welcome_screen.py +++ /dev/null @@ -1,14 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.reference import Snowflake - - -class WelcomeScreenChannel(ObjDict): - channel_id: Snowflake - description: str - emoji_id: Snowflake | None - emoji_name: str | None - - -class WelcomeScreen(ObjDict): - description: str | None - welcome_channel: list[WelcomeScreenChannel] diff --git a/dislord/discord/resources/guild_scheduled_event/__init__.py b/dislord/discord/resources/guild_scheduled_event/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/resources/guild_scheduled_event/guild_scheduled_event.py b/dislord/discord/resources/guild_scheduled_event/guild_scheduled_event.py deleted file mode 100644 index 22e7bf49..00000000 --- a/dislord/discord/resources/guild_scheduled_event/guild_scheduled_event.py +++ /dev/null @@ -1,45 +0,0 @@ -from enum import IntEnum - -from dislord.types import ObjDict -from dislord.discord.resources.user.user import User -from dislord.discord.reference import Snowflake, Missing, ISOTimestamp - - -class GuildScheduledEventEntityMetadata(ObjDict): - location: str | Missing - - -class GuildScheduledEventStatus(IntEnum): - SCHEDULED = 1 - ACTIVE = 2 - COMPLETED = 3 - CANCELED = 4 - - -class GuildScheduledEventEntityType(IntEnum): - STAGE_INSTANCE = 1 - VOICE = 2 - EXTERNAL = 3 - - -class GuildScheduledEventPrivacyLevel(IntEnum): - GUILD_ONLY = 2 - - -class GuildScheduledEvent(ObjDict): - id: Snowflake - guild_id: Snowflake - channel_id: Snowflake | None - creator_id: Snowflake | Missing | None - name: str - description: str | Missing | None - scheduled_start_time: ISOTimestamp - scheduled_ent_time: ISOTimestamp | None - privacy_level: GuildScheduledEventPrivacyLevel - status: GuildScheduledEventStatus - entity_type: GuildScheduledEventEntityType - entity_id: Snowflake | None - entity_metadata: GuildScheduledEventEntityMetadata | None - creator: User | Missing - user_count: int | Missing - image: str | Missing | None diff --git a/dislord/discord/resources/guild_scheduled_event/guild_scheduled_event_user.py b/dislord/discord/resources/guild_scheduled_event/guild_scheduled_event_user.py deleted file mode 100644 index e576f96b..00000000 --- a/dislord/discord/resources/guild_scheduled_event/guild_scheduled_event_user.py +++ /dev/null @@ -1,10 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.resources.guild.guild_member import GuildMember -from dislord.discord.resources.user.user import User -from dislord.discord.reference import Snowflake, Missing - - -class GuildScheduledEventUser(ObjDict): - guild_scheduled_event_id: Snowflake - user: User - member: GuildMember | Missing diff --git a/dislord/discord/resources/guild_template/__init__.py b/dislord/discord/resources/guild_template/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/resources/guild_template/guild_template.py b/dislord/discord/resources/guild_template/guild_template.py deleted file mode 100644 index 209b2f1d..00000000 --- a/dislord/discord/resources/guild_template/guild_template.py +++ /dev/null @@ -1,18 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.resources.guild.guild import PartialGuild -from dislord.discord.resources.user.user import User -from dislord.discord.reference import Snowflake, ISOTimestamp - - -class GuildTemplate(ObjDict): - code: str - name: str - description: str | None - usage_count: int - creator_id: Snowflake - creator: User - created_at: ISOTimestamp - updated_at: ISOTimestamp - source_guild_id: Snowflake - serialized_source_guild: PartialGuild - is_dirty: bool | None diff --git a/dislord/discord/resources/invite/__init__.py b/dislord/discord/resources/invite/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/resources/invite/invite.py b/dislord/discord/resources/invite/invite.py deleted file mode 100644 index 1d4599ca..00000000 --- a/dislord/discord/resources/invite/invite.py +++ /dev/null @@ -1,37 +0,0 @@ -from enum import IntEnum - -from dislord.types import ObjDict -from dislord.discord.resources.application.models import PartialApplication -from dislord.discord.resources.channel.channel import PartialChannel -from dislord.discord.resources.guild.guild import PartialGuild -from dislord.discord.resources.guild_scheduled_event.guild_scheduled_event import GuildScheduledEvent -from dislord.discord.resources.invite.invite_stage_instance import InviteStageInstance -from dislord.discord.resources.user.user import User -from dislord.discord.reference import Missing, ISOTimestamp - - -class InviteTargetType(IntEnum): - STREAM = 1 - EMBEDDED_APPLICATION = 2 - - -class InviteType(IntEnum): - GUILD = 0 - GROUP_DM = 1 - FRIEND = 2 - - -class Invite(ObjDict): - type: InviteType - code: str - guild: PartialGuild | Missing - channel: PartialChannel | None - inviter: User | Missing - target_type: InviteTargetType | Missing - target_user: User | Missing - target_application: PartialApplication | Missing - approximate_presence_count = int | Missing - approximate_member_count: int | Missing - expires_at: ISOTimestamp | Missing | None - stage_instance: InviteStageInstance | Missing - guild_scheduled_event: GuildScheduledEvent | Missing diff --git a/dislord/discord/resources/invite/invite_metadata.py b/dislord/discord/resources/invite/invite_metadata.py deleted file mode 100644 index 331ce529..00000000 --- a/dislord/discord/resources/invite/invite_metadata.py +++ /dev/null @@ -1,10 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.reference import ISOTimestamp - - -class InviteMetadata(ObjDict): - uses: int - max_uses: int - max_age: int - temporary: bool - created_at: ISOTimestamp diff --git a/dislord/discord/resources/invite/invite_stage_instance.py b/dislord/discord/resources/invite/invite_stage_instance.py deleted file mode 100644 index c73215be..00000000 --- a/dislord/discord/resources/invite/invite_stage_instance.py +++ /dev/null @@ -1,9 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.resources.guild.guild_member import PartialGuildMember - - -class InviteStageInstance(ObjDict): - members: list[PartialGuildMember] - participant_count: int - speaker_count: int - topic: str diff --git a/dislord/discord/resources/poll/__init__.py b/dislord/discord/resources/poll/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/resources/poll/layout_type.py b/dislord/discord/resources/poll/layout_type.py deleted file mode 100644 index 779e8eee..00000000 --- a/dislord/discord/resources/poll/layout_type.py +++ /dev/null @@ -1,5 +0,0 @@ -from enum import IntEnum - - -class LayoutType(IntEnum): - DEFAULT = 1 diff --git a/dislord/discord/resources/poll/poll.py b/dislord/discord/resources/poll/poll.py deleted file mode 100644 index cf1ada38..00000000 --- a/dislord/discord/resources/poll/poll.py +++ /dev/null @@ -1,15 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.resources.poll.layout_type import LayoutType -from dislord.discord.resources.poll.poll_answer import PollAnswer -from dislord.discord.resources.poll.poll_media import PollMedia -from dislord.discord.resources.poll.poll_results import PollResults -from dislord.discord.reference import ISOTimestamp, Missing - - -class Poll(ObjDict): - question: PollMedia - answers: list[PollAnswer] - expiry: ISOTimestamp | None - allow_multiset: bool - layout_type: LayoutType - results: PollResults | Missing \ No newline at end of file diff --git a/dislord/discord/resources/poll/poll_answer.py b/dislord/discord/resources/poll/poll_answer.py deleted file mode 100644 index 9ccde1c6..00000000 --- a/dislord/discord/resources/poll/poll_answer.py +++ /dev/null @@ -1,7 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.resources.poll.poll_media import PollMedia - - -class PollAnswer(ObjDict): - answer_id: int - poll_media: PollMedia diff --git a/dislord/discord/resources/poll/poll_create_request.py b/dislord/discord/resources/poll/poll_create_request.py deleted file mode 100644 index a06279c1..00000000 --- a/dislord/discord/resources/poll/poll_create_request.py +++ /dev/null @@ -1,13 +0,0 @@ -from dislord.discord.reference import Missing -from dislord.discord.resources.poll.layout_type import LayoutType -from dislord.discord.resources.poll.poll_answer import PollAnswer -from dislord.discord.resources.poll.poll_media import PollMedia -from dislord.types import ObjDict - - -class PollCreateRequest(ObjDict): - question: PollMedia - answers: list[PollAnswer] - duration: int - allow_multiset: bool - layout_type: LayoutType | Missing diff --git a/dislord/discord/resources/poll/poll_media.py b/dislord/discord/resources/poll/poll_media.py deleted file mode 100644 index 0cad151b..00000000 --- a/dislord/discord/resources/poll/poll_media.py +++ /dev/null @@ -1,8 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.resources.emoji.emoji import PartialEmoji -from dislord.discord.reference import Missing - - -class PollMedia(ObjDict): - text: str | Missing - emoji: PartialEmoji | Missing diff --git a/dislord/discord/resources/poll/poll_results.py b/dislord/discord/resources/poll/poll_results.py deleted file mode 100644 index 374ead77..00000000 --- a/dislord/discord/resources/poll/poll_results.py +++ /dev/null @@ -1,12 +0,0 @@ -from dislord.types import ObjDict - - -class PollAnswerCount(ObjDict): - id: int - count: int - me_voted: bool - - -class PollResults(ObjDict): - is_finalized: bool - answer_counts: list[PollAnswerCount] diff --git a/dislord/discord/resources/stage_instance/__init__.py b/dislord/discord/resources/stage_instance/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/resources/stage_instance/stage_instance.py b/dislord/discord/resources/stage_instance/stage_instance.py deleted file mode 100644 index df6c657c..00000000 --- a/dislord/discord/resources/stage_instance/stage_instance.py +++ /dev/null @@ -1,17 +0,0 @@ -from enum import IntEnum - -from dislord.types import ObjDict -from dislord.discord.reference import Snowflake - - -class PrivacyLevel(IntEnum): - PUBLIC = 1 - GUILD_ONLY = 2 - - -class StageInstance(ObjDict): - id: Snowflake - guild_id: Snowflake - channel_id: Snowflake - topic: str - privacy_level: PrivacyLevel diff --git a/dislord/discord/resources/sticker/__init__.py b/dislord/discord/resources/sticker/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/resources/sticker/sticker.py b/dislord/discord/resources/sticker/sticker.py deleted file mode 100644 index 2db95a76..00000000 --- a/dislord/discord/resources/sticker/sticker.py +++ /dev/null @@ -1,32 +0,0 @@ -from enum import IntEnum - -from dislord.types import ObjDict -from dislord.discord.resources.user.user import User -from dislord.discord.reference import Snowflake, Missing - - -class StickerFormatType(IntEnum): - PNG = 1 - APNG = 2 - LOTTIE = 3 - GIF = 4 - - -class StickerType(IntEnum): - STANDARD = 1 - GUILD = 2 - - -class Sticker(ObjDict): - id: Snowflake - pack_id: Snowflake | Missing - name: str - description: str | None - tags: str - asset: str | Missing # Deprecated - type: StickerType - format_type: StickerFormatType - available: bool | Missing - guild_id: Snowflake | Missing - user: User | Missing - sort_value: int | Missing diff --git a/dislord/discord/resources/sticker/sticker_item.py b/dislord/discord/resources/sticker/sticker_item.py deleted file mode 100644 index 03a5cf32..00000000 --- a/dislord/discord/resources/sticker/sticker_item.py +++ /dev/null @@ -1,9 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.resources.sticker.sticker import StickerFormatType -from dislord.discord.reference import Snowflake - - -class StickerItem(ObjDict): - id: Snowflake - name: str - format_type: StickerFormatType diff --git a/dislord/discord/resources/sticker/sticker_pack.py b/dislord/discord/resources/sticker/sticker_pack.py deleted file mode 100644 index 179f502f..00000000 --- a/dislord/discord/resources/sticker/sticker_pack.py +++ /dev/null @@ -1,13 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.resources.sticker.sticker import Sticker -from dislord.discord.reference import Snowflake, Missing - - -class StickerPack(ObjDict): - id: Snowflake - stickers: list[Sticker] - name: str - sku_id: Snowflake - cover_sticker_id: Snowflake | Missing - description: str - banner_asset_id: Snowflake | Missing diff --git a/dislord/discord/resources/user/__init__.py b/dislord/discord/resources/user/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/resources/user/application_role_connection.py b/dislord/discord/resources/user/application_role_connection.py deleted file mode 100644 index 96d0efef..00000000 --- a/dislord/discord/resources/user/application_role_connection.py +++ /dev/null @@ -1,8 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.resources.application_role_connection_metadata.models import ApplicationRoleConnectionMetadata - - -class ApplicationRoleConnection(ObjDict): - platform_name: str | None - platform_username: str | None - metadata: dict[ApplicationRoleConnectionMetadata, str] diff --git a/dislord/discord/resources/user/avatar_decoration_data.py b/dislord/discord/resources/user/avatar_decoration_data.py deleted file mode 100644 index 11c65cdd..00000000 --- a/dislord/discord/resources/user/avatar_decoration_data.py +++ /dev/null @@ -1,7 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.reference import Snowflake - - -class AvatarDecorationData(ObjDict): - asset: str - sku_id: Snowflake diff --git a/dislord/discord/resources/user/connection.py b/dislord/discord/resources/user/connection.py deleted file mode 100644 index 308c4305..00000000 --- a/dislord/discord/resources/user/connection.py +++ /dev/null @@ -1,23 +0,0 @@ -from enum import IntEnum - -from dislord.types import ObjDict -from dislord.discord.resources.guild.integration import Integration -from dislord.discord.reference import Missing - - -class VisibilityType(IntEnum): - NONE = 0 - EVERYONE = 1 - - -class Connection(ObjDict): - id: str - name: str - type: str - revoked: bool | Missing - integrations: list[Integration] | Missing - verified: bool - friend_sync: bool - show_activity: bool - two_way_link: bool - visibility: VisibilityType diff --git a/dislord/discord/resources/user/user.py b/dislord/discord/resources/user/user.py deleted file mode 100644 index 897ec589..00000000 --- a/dislord/discord/resources/user/user.py +++ /dev/null @@ -1,52 +0,0 @@ -from enum import IntEnum, IntFlag - -from dislord.types import ObjDict -from dislord.discord.reference import Snowflake, Missing, Locale - - -class PremiumType(IntEnum): - NONE = 0 - NITRO_CLASSIC = 1 - NITRO = 2 - NITRO_BASIC = 3 - - -class UserFlags(IntFlag): - STAFF = 1 << 0 - PARTNER = 1 << 1 - HYPESQUAD = 1 << 2 - BUG_HUNTER_LEVEL_1 = 1 << 3 - HYPESQUAD_ONLINE_HOUSE_1 = 1 << 6 - HYPESQUAD_ONLINE_HOUSE_2 = 1 << 7 - HYPESQUAD_ONLINE_HOUSE_3 = 1 << 8 - PREMIUM_EARLY_SUPPORTER = 1 << 9 - TEAM_PSEUDO_USER = 1 << 10 - BUG_HUNTER_LEVEL_2 = 1 << 14 - VERIFIED_BOT = 1 << 16 - VERIFIED_DEVELOPER = 1 << 17 - CERTIFIED_MODERATOR = 1 << 18 - BOT_HTTP_INTERACTIONS = 1 << 19 - ACTIVE_DEVELOPER = 1 << 22 - - -class PartialUser(ObjDict): - id: Snowflake - username: str - discriminator: str - avatar: str | None - - -class User(PartialUser): - global_name: str | None - bot: bool | Missing - system: bool | Missing - mfa_enabled: bool | Missing - banner: str | Missing | None - accent_color: int | Missing | None - locale: Locale | Missing - verified: bool | Missing - email: str | Missing | None - flags: UserFlags | Missing - premium_type: PremiumType | Missing - public_flags: UserFlags | Missing - avatar_decoration: str | Missing | None diff --git a/dislord/discord/resources/voice/__init__.py b/dislord/discord/resources/voice/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/resources/voice/voice_region.py b/dislord/discord/resources/voice/voice_region.py deleted file mode 100644 index c8007216..00000000 --- a/dislord/discord/resources/voice/voice_region.py +++ /dev/null @@ -1,9 +0,0 @@ -from dislord.types import ObjDict - - -class VoiceRegion(ObjDict): - id: str - name: str - optimal: bool - deprecated: bool - custom: bool diff --git a/dislord/discord/resources/voice/voice_state.py b/dislord/discord/resources/voice/voice_state.py deleted file mode 100644 index 2fb8a409..00000000 --- a/dislord/discord/resources/voice/voice_state.py +++ /dev/null @@ -1,19 +0,0 @@ -from dislord.types import ObjDict -from dislord.discord.resources.guild.guild_member import GuildMember -from dislord.discord.reference import Snowflake, Missing, ISOTimestamp - - -class VoiceState(ObjDict): - guild_id: Snowflake | Missing - channel_id: Snowflake | None - user_id: Snowflake - member: GuildMember | Missing - session_id: str - deaf: bool - mute: bool - self_deaf: bool - self_mute: bool - self_stream: bool | Missing - self_video: bool - suppress: bool - request_to_speak_timestamp: ISOTimestamp | None diff --git a/dislord/discord/resources/webhook/__init__.py b/dislord/discord/resources/webhook/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/resources/webhook/webhook.py b/dislord/discord/resources/webhook/webhook.py deleted file mode 100644 index 7134dfce..00000000 --- a/dislord/discord/resources/webhook/webhook.py +++ /dev/null @@ -1,27 +0,0 @@ -from enum import IntEnum - -from dislord.types import ObjDict -from dislord.discord.resources.channel.channel import PartialChannel -from dislord.discord.resources.guild.guild import PartialGuild -from dislord.discord.resources.user.user import User -from dislord.discord.reference import Snowflake, Missing - - -class WebhookType(IntEnum): - INCOMING = 1 - CHANNEL_FOLLOWER = 2 - APPLICATION = 3 - - -class Webhook(ObjDict): - id: Snowflake - type: WebhookType - guild_id: Snowflake | Missing | None - channel_id: Snowflake | None - user: User | None - avatar: str | None - token: str | Missing - application_id: Snowflake | None - source_guild: PartialGuild | Missing - source_channel: PartialChannel | Missing - url: str | Missing diff --git a/dislord/discord/topics/__init__.py b/dislord/discord/topics/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/topics/permissions/__init__.py b/dislord/discord/topics/permissions/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/topics/permissions/permissions.py b/dislord/discord/topics/permissions/permissions.py deleted file mode 100644 index 709a1630..00000000 --- a/dislord/discord/topics/permissions/permissions.py +++ /dev/null @@ -1,52 +0,0 @@ -from enum import IntFlag - - -class Permission(IntFlag): - CREATE_INSTANT_INVITE = 1 << 0 - KICK_MEMBERS = 1 << 1 - BAN_MEMBERS = 1 << 2 - ADMINISTRATOR = 1 << 3 - MANAGE_CHANNELS = 1 << 4 - MANAGE_GUILD = 1 << 5 - ADD_REACTIONS = 1 << 6 - VIEW_AUDIT_LOG = 1 << 7 - PRIORITY_SPEAKER = 1 << 8 - STREAM = 1 << 9 - VIEW_CHANNEL = 1 << 10 - SEND_MESSAGES = 1 << 11 - SEND_TTS_MESSAGES = 1 << 12 - MANAGE_MESSAGES = 1 << 13 - EMBED_LINKS = 1 << 14 - ATTACH_FILES = 1 << 15 - READ_MESSAGE_HISTORY = 1 << 16 - MENTION_EVERYONE = 1 << 17 - USE_EXTERNAL_EMOJIS = 1 << 18 - VIEW_GUILD_INSIGHTS = 1 << 19 - CONNECT = 1 << 20 - SPEAK = 1 << 21 - MUTE_MEMBERS = 1 << 22 - DEAFEN_MEMBERS = 1 << 23 - MOVE_MEMBERS = 1 << 24 - USE_VAD = 1 << 25 - CHANGE_NICKNAME = 1 << 26 - MANAGE_NICKNAMES = 1 << 27 - MANAGE_ROLES = 1 << 28 - MANAGE_WEBHOOKS = 1 << 29 - MANAGE_GUILD_EXPRESSIONS = 1 << 30 - USE_APPLICATION_COMMANDS = 1 << 31 - REQUEST_TO_SPEAK = 1 << 32 - MANAGE_EVENTS = 1 << 33 - MANAGE_THREADS = 1 << 34 - CREATE_PUBLIC_THREADS = 1 << 35 - CREATE_PRIVATE_THREADS = 1 << 36 - USE_EXTERNAL_STICKERS = 1 << 37 - SEND_MESSAGES_IN_THREADS = 1 << 38 - USE_EMBEDDED_ACTIVITIES = 1 << 39 - MODERATE_MEMBERS = 1 << 40 - VIEW_CREATOR_MONETIZATION_ANALYTICS = 1 << 41 - USE_SOUNDBOARD = 1 << 42 - CREATE_GUILD_EXPRESSIONS = 1 << 43 - CREATE_EVENTS = 1 << 44 - USE_EXTERNAL_SOUNDS = 1 << 45 - SEND_VOICE_MESSAGES = 1 << 46 - SEND_POLLS = 1 << 49 diff --git a/dislord/discord/topics/permissions/role.py b/dislord/discord/topics/permissions/role.py deleted file mode 100644 index 920431ae..00000000 --- a/dislord/discord/topics/permissions/role.py +++ /dev/null @@ -1,32 +0,0 @@ -from enum import IntFlag - -from dislord.types import ObjDict -from dislord.discord.reference import Snowflake, Missing - - -class RoleTags(ObjDict): - bot_id: Snowflake | Missing - integration_id: Snowflake | Missing - premium_subscriber: Missing | None - subscription_listing_id: Snowflake | Missing - available_for_purchase: Missing | None - guild_connections: Missing | None - - -class RoleFlags(IntFlag): - IN_PROMPT = 1 << 0 - - -class Role(ObjDict): - id: Snowflake - name: str - color: int - hoist: bool - icon: str | Missing | None - unicode_emoji: str | Missing | None - position: int - permissions: str - managed: bool - mentionable: bool - tags: RoleTags | Missing - flags: RoleFlags diff --git a/dislord/discord/topics/teams/__init__.py b/dislord/discord/topics/teams/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/discord/topics/teams/data_models.py b/dislord/discord/topics/teams/data_models.py deleted file mode 100644 index 0982f868..00000000 --- a/dislord/discord/topics/teams/data_models.py +++ /dev/null @@ -1,26 +0,0 @@ -from enum import IntEnum - -from dislord.types import ObjDict -from dislord.discord.reference import Snowflake -from dislord.discord.resources.user.user import PartialUser -from dislord.discord.topics.teams.team_member_roles import TeamMemberRoleType - - -class MembershipState(IntEnum): - INVITED = 1 - ACCEPTED = 2 - - -class TeamMember(ObjDict): - membership_state: int - team_id: Snowflake - user: PartialUser - role: TeamMemberRoleType - - -class Team(ObjDict): - icon: str | None - id: Snowflake - members: list[TeamMember] - name: str - owner_user_id: Snowflake diff --git a/dislord/discord/topics/teams/team_member_roles.py b/dislord/discord/topics/teams/team_member_roles.py deleted file mode 100644 index 31614794..00000000 --- a/dislord/discord/topics/teams/team_member_roles.py +++ /dev/null @@ -1,8 +0,0 @@ -from enum import Enum - - -class TeamMemberRoleType(Enum): - OWNER = None - ADMIN = "admin" - DEVELOPER = "developer" - READ_ONLY = "read_only" diff --git a/dislord/dpyadapter/__init__.py b/dislord/dpyadapter/__init__.py deleted file mode 100644 index d887f292..00000000 --- a/dislord/dpyadapter/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import handlers diff --git a/dislord/dpyadapter/handlers.py b/dislord/dpyadapter/handlers.py deleted file mode 100644 index 2a5d02e4..00000000 --- a/dislord/dpyadapter/handlers.py +++ /dev/null @@ -1,15 +0,0 @@ -import discord - -from dislord.discord.interactions.receiving_and_responding.interaction_response import InteractionResponse, \ - InteractionCallbackType -from dislord.discord.resources.channel.message import MessageFlags - - -async def handle_response(response: InteractionResponse, dpy_interaction: discord.Interaction): - match response.type: - case InteractionCallbackType.PONG: - await dpy_interaction.response.pong() - case InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE: - await dpy_interaction.response.send_message(ephemeral=( - response.data.get("flags") is not None and MessageFlags.EPHEMERAL in response.data.get("flags")), - **response.data) diff --git a/dislord/error.py b/dislord/error.py deleted file mode 100644 index ec164482..00000000 --- a/dislord/error.py +++ /dev/null @@ -1,6 +0,0 @@ -class StateConfigurationException(Exception): - MISSING_PUBLIC_TOKEN = "Public token has not been configured. Please use dislord.configure(...)" - - -class DiscordApiException(Exception): - UNKNOWN_INTERACTION_TYPE = "Unknown interaction type %s" diff --git a/dislord/group.py b/dislord/group.py deleted file mode 100644 index dd8586e6..00000000 --- a/dislord/group.py +++ /dev/null @@ -1,47 +0,0 @@ -from typing import Callable - -from dislord.discord.interactions.application_commands.enums import ApplicationCommandOptionType -from dislord.discord.interactions.application_commands.models import ApplicationCommandOption -from dislord.discord.interactions.receiving_and_responding.interaction import Interaction -from dislord.discord.reference import Snowflake - - -class CommandGroup: - name: str - description: str - dm_permission: bool - nsfw: bool - guild_id: Snowflake - parent: 'CommandGroup' = None - commands: dict[str, ApplicationCommandOption] = {} - command_callbacks: dict[str, Callable] = {} - - def __init__(self, name: str = None, description: str = None, - dm_permission: bool = True, nsfw: bool = False, guild_id: Snowflake = None, - parent: 'CommandGroup' = None): - self.name = name - self.description = description - self.parent = parent - self.dm_permission = dm_permission - self.nsfw = nsfw - self.guild_id = guild_id - - def add_command(self, command: ApplicationCommandOption, callback: Callable): - self.commands[command.name] = command - self.command_callbacks[command.name] = callback - - def command(self, *, name, description, options: list[ApplicationCommandOption] = None): - def decorator(func): - self.add_command(ApplicationCommandOption(name=name, description=description, - type=ApplicationCommandOptionType.SUB_COMMAND, - options=options), func) - return func - - return decorator - - def callback(self, interaction: Interaction, **kwargs): - command_name = interaction.data.options[0].name - kwargs = {} - for option in interaction.data.options[0].options: - kwargs[option.name] = option.value - return self.command_callbacks[command_name](interaction, **kwargs) diff --git a/dislord/model/__init__.py b/dislord/model/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/model/api.py b/dislord/model/api.py deleted file mode 100644 index c01bf14f..00000000 --- a/dislord/model/api.py +++ /dev/null @@ -1,30 +0,0 @@ -import json -from http.client import OK, UNAUTHORIZED - -from dislord.model.base import EnhancedJSONEncoder - - -class HttpResponse: - status_code: int - body: [dict, str] - headers: dict - - def __init__(self, body, *, headers=None): - self.body = body - self.headers = headers - - def as_serverless_response(self): - return {"statusCode": int(self.status_code), - "body": json.dumps(self.body, cls=EnhancedJSONEncoder), - "headers": self.headers} - - def as_server_response(self): - return self.body, int(self.status_code) - - -class HttpOk(HttpResponse): - status_code = OK - - -class HttpUnauthorized(HttpResponse): - status_code = UNAUTHORIZED diff --git a/dislord/model/application.py b/dislord/model/application.py deleted file mode 100644 index 761e70b3..00000000 --- a/dislord/model/application.py +++ /dev/null @@ -1,13 +0,0 @@ -from dislord.discord.resources.application.models import Application as ApplicationPayload -from dislord.model.base import BaseModel - - -class Application(BaseModel): - payload: ApplicationPayload - - @staticmethod - def from_payload(payload: ApplicationPayload) -> 'Application': - return Application( - payload=payload - ) - diff --git a/dislord/model/base.py b/dislord/model/base.py deleted file mode 100644 index 595a11b0..00000000 --- a/dislord/model/base.py +++ /dev/null @@ -1,242 +0,0 @@ -import abc -import copy -import json -import typing -from enum import Enum, EnumType, IntEnum -from functools import reduce -from types import UnionType -from typing import get_type_hints, Union, Self - -from dislord.discord.reference import Missing -from dislord.types import ObjDict - - -def cast(obj, type_hint, param_name=None, client=None): - # None - if type_hint is None and (obj is None or obj is Missing): - return obj - - # Missing - if type_hint is Missing and (obj is None or obj is Missing): - return obj - - if type_hint is None: - print("type_hint is None but obj is {}".format(obj)) - return obj - - if getattr(type_hint, '__origin__', None) == typing.Literal: - if obj in type_hint.__args__: - return obj - else: - raise ValueError(f"{obj} is not in {type_hint}") - - # Union - if isinstance(type_hint, UnionType): - errors = [] - for arg in type_hint.__args__: - try: - return cast(obj, arg, param_name, client) - except Exception as err: - errors.append(err) - raise RuntimeError(f"None of the union type hints succeeded {errors} for {param_name}") - - # List - if isinstance(type_hint, list): - return list(obj) - elif getattr(type_hint, '__origin__', None) is list: - return [cast(o, type_hint.__args__[0], param_name, client) for o in obj] - - # Dict - if isinstance(type_hint, dict): - return dict(obj) - elif getattr(type_hint, '__origin__', None) is dict: - return { - cast(ok, type_hint.__args__[0], param_name, client): cast(obj.get(ok), type_hint.__args__[1], ok, - client) - for ok in obj.keys()} - - # Literal - if not isinstance(type_hint, type): - if obj == type_hint: - return obj - else: - raise RuntimeError(f"{param_name}={obj} is not equal to Literal {type_hint}") - - # BaseModel - if issubclass(type_hint, BaseModel): - return type_hint.from_dict(obj, client) - - if issubclass(type_hint, ObjDict): - if not isinstance(obj, dict): - raise TypeError(f"{type_hint} is not a dict") - return type_hint(**{k: cast(v, - get_type_hints(type_hint).get(k), k, client) - for k, v in obj.items()}) - - # Enum, str, int, float - if issubclass(type_hint, IntEnum): - return type_hint(int(obj)) - - if obj is not None and not isinstance(obj, Missing): - return type_hint(obj) - else: - raise RuntimeError(f"{obj} cannot be converted to {type_hint}") - - -class AutoInitMeta(type): - def __new__(cls, name, bases, dct): - # Extract annotations (type hints) - annotations = dct.get("__annotations__", {}) | reduce(lambda a, b: a | b, - [getattr(t, "__annotations__", {}) for t in bases], {}) - - # Define the __init__ method - def __init__(self, *args, **kwargs): - for key, value in annotations.items(): - if key in kwargs: - setattr(self, key, kwargs[key]) - else: - setattr(self, key, None) - - # Assign positional arguments - for key, arg in zip(annotations, args): - setattr(self, key, arg) - - dct['__init__'] = __init__ - - # Create the new class - return super().__new__(cls, name, bases, dct) - - -class BaseModel(metaclass=AutoInitMeta): - def __init__(self, *args, **kwargs): - # Generated by metaclass - pass - - @staticmethod - @abc.abstractmethod - def from_payload(payload: type[dict]) -> type['BaseModel']: - pass - - @classmethod - def from_dict(cls, env, client): - type_hints = get_type_hints(cls) - if cls == type(env): - return env - - params = {} - for p, hint in type_hints.items(): - if isinstance(env, dict): - prop = env.get(p, Missing()) - else: - prop = getattr(env, p, Missing()) - params[p] = cast(prop, hint, p) - obj = cls(**params) # noqa - obj.client = client - return obj - - @classmethod - def from_kwargs(cls, *, client=None, **kwargs): - return cls.from_dict(kwargs, client) - - def asdict(self, *, dict_factory=dict): - """Return the fields of a dataclass instance as a new dictionary mapping - field names to field values. - - Example usage:: - - class C: - x: int - y: int - - c = C(1, 2) - assert asdict(c) == {'x': 1, 'y': 2} - - If given, 'dict_factory' will be used instead of built-in dict. - The function applies recursively to field values that are - dataclass instances. This will also look into built-in containers: - tuples, lists, and dicts. - """ - return BaseModel._asdict_inner(self, dict_factory) - - @staticmethod - def _asdict_inner(obj, dict_factory): - if obj is Missing: - return None - if isinstance(obj, BaseModel): - result = [] - for name, value in obj.__annotations__.items(): - result.append((BaseModel._asdict_inner(name, dict_factory), - BaseModel._asdict_inner(getattr(obj, name), dict_factory))) - return dict_factory(result) - elif isinstance(obj, tuple) and hasattr(obj, '_fields'): - # obj is a namedtuple. Recurse into it, but the returned - # object is another namedtuple of the same type. This is - # similar to how other list- or tuple-derived classes are - # treated (see below), but we just need to create them - # differently because a namedtuple's __init__ needs to be - # called differently (see bpo-34363). - - # I'm not using namedtuple's _asdict() - # method, because: - # - it does not recurse in to the namedtuple fields and - # convert them to dicts (using dict_factory). - # - I don't actually want to return a dict here. The main - # use case here is json.dumps, and it handles converting - # namedtuples to lists. Admittedly we're losing some - # information here when we produce a json list instead of a - # dict. Note that if we returned dicts here instead of - # namedtuples, we could no longer call asdict() on a data - # structure where a namedtuple was used as a dict key. - - return type(obj)(*[BaseModel._asdict_inner(v, dict_factory) for v in obj]) - elif isinstance(obj, (list, tuple)): - # Assume we can create an object of this type by passing in a - # generator (which is not true for namedtuples, handled - # above). - return type(obj)(BaseModel._asdict_inner(v, dict_factory) for v in obj) - elif isinstance(obj, dict): - return type(obj)((BaseModel._asdict_inner(k, dict_factory), - BaseModel._asdict_inner(v, dict_factory)) - for k, v in obj.items()) - else: - return copy.deepcopy(obj) - - def __eq__(self, other): - result = True - for eq_attr in get_type_hints(self.__class__): - self_attr = getattr(self, eq_attr, None) - other_attr = getattr(other, eq_attr, None) - result = result and compare_missing_none(self_attr, other_attr) - return result - - -class EnhancedJSONEncoder(json.JSONEncoder): - def default(self, o): - if issubclass(type(o), ObjDict): - return {k: self.default(v) for k, v in o.items()} - if issubclass(type(o), BaseModel): - return o.asdict() - if issubclass(type(o), Enum): - return o.value - if o is Missing: - return None - if isinstance(o, list): - return [self.default(v) for v in o] - # if isinstance(o, type): - return f"DEBUG: {o}" - # return super().default(o) - - -def compare_missing_none(obj1, obj2): - obj1_is_none_or_missing = obj1 is None or obj1 is Missing - obj2_is_none_or_missing = obj2 is None or obj2 is Missing - if obj1_is_none_or_missing and obj2_is_none_or_missing: - return True - else: - if isinstance(obj1, Enum) and isinstance(obj2, Enum): - return obj1.value == obj2.value - else: - return obj1.__eq__(obj2) - - -HexColor = type(int) diff --git a/dislord/model/channel.py b/dislord/model/channel.py deleted file mode 100644 index f7511275..00000000 --- a/dislord/model/channel.py +++ /dev/null @@ -1,12 +0,0 @@ -from dislord.discord.resources.channel.channel import Channel as ChannelPayload -from dislord.model.base import BaseModel - - -class Channel(BaseModel): - payload: ChannelPayload - - @staticmethod - def from_payload(payload: ChannelPayload) -> 'Channel': - return Channel( - payload=payload - ) diff --git a/dislord/model/commands.py b/dislord/model/commands.py deleted file mode 100644 index 9f86d3c1..00000000 --- a/dislord/model/commands.py +++ /dev/null @@ -1,35 +0,0 @@ -from discord import Permissions - -from dislord.discord.interactions.application_commands.enums import ApplicationCommandType -from dislord.discord.interactions.application_commands.models import ApplicationCommand as ApplicationCommandPayload, \ - ApplicationCommandOption -from dislord.discord.interactions.receiving_and_responding.interaction import InteractionContextType -from dislord.discord.reference import Snowflake, Missing, Locale -from dislord.discord.resources.application.enums import ApplicationIntegrationType -from dislord.model.base import BaseModel - - -class ApplicationCommand(ApplicationCommandPayload): - - @staticmethod - def from_payload(payload: ApplicationCommandPayload) -> 'ApplicationCommand': - return ApplicationCommand( - **payload - ) - - def to_payload(self) -> ApplicationCommandPayload: - return self - - def __eq__(self, other): - eq_list = ['guild_id', 'name', 'description', 'type', 'name_localization', 'description_localizations', - 'options', 'default_member_permissions', 'dm_permission', 'default_permission', 'nsfw'] - result = True - for eq_attr in eq_list: - self_attr = getattr(self, eq_attr, None) - other_attr = getattr(other, eq_attr, None) - result = result and (self_attr == other_attr or self_attr is other_attr) # compare_missing_none(self_attr, other_attr) - return result - - def __post_init__(self): - if self.guild_id is not None and self.guild_id is not Missing: - self.dm_permission = None diff --git a/dislord/model/components.py b/dislord/model/components.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dislord/model/guild.py b/dislord/model/guild.py deleted file mode 100644 index 0da2b408..00000000 --- a/dislord/model/guild.py +++ /dev/null @@ -1,13 +0,0 @@ -from dislord.discord.resources.guild.guild import Guild as GuildPayload -from dislord.model.base import BaseModel - - -class Guild(GuildPayload): - - @staticmethod - def from_payload(payload: GuildPayload) -> 'Guild': - return Guild( - **payload - ) - - diff --git a/dislord/model/user.py b/dislord/model/user.py deleted file mode 100644 index c841f6ae..00000000 --- a/dislord/model/user.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import Self - -from dislord.discord.resources.user.user import User as UserPayload -from dislord.model.base import BaseModel - - -class User(BaseModel): - payload: UserPayload - - @staticmethod - def from_payload(payload: UserPayload) -> 'User': - return User( - payload=payload - ) diff --git a/dislord/server.py b/dislord/server.py deleted file mode 100644 index ff5bf128..00000000 --- a/dislord/server.py +++ /dev/null @@ -1,42 +0,0 @@ -from .client import ApplicationClient - -try: - from flask import Flask, request - app = Flask(__name__) -except ImportError: - Flask = None - request = None - - class FakeFlask: - @staticmethod - def route(self, *args, **kwargs): # noqa - def decorator(func): - return func - return decorator - - def run(self, *args, **kwargs): - raise RuntimeError("flask library needed in order to use server") - - app = FakeFlask() - -__application_client: ApplicationClient - - -@app.route("/", methods=["POST"]) -async def interactions_endpoint(): - raw_request = request.json - signature = request.headers.get('X-Signature-Ed25519') - timestamp = request.headers.get('X-Signature-Timestamp') - print(f"👉 Request: {raw_request}") - response = __application_client.verified_interact(raw_request, signature, timestamp).as_server_response() - print(f"🫴 Response: {response}") - return response - - -def start_server(application_client, **kwargs): - global __application_client - __application_client = application_client - app.run(**kwargs) - - - diff --git a/dislord/types.py b/dislord/types.py deleted file mode 100644 index 0809cc5d..00000000 --- a/dislord/types.py +++ /dev/null @@ -1,18 +0,0 @@ -class ObjDict(dict): - def __getattr__(self, name): - try: - return self.get(name) - except KeyError: - raise AttributeError(f"'ObjDict' object has no attribute '{name}'") - - def __setattr__(self, name, value): - self[name] = value - - def __eq__(self, other): - if isinstance(other, ObjDict): - for k, v in self.items(): - if v != other.get(k): - return False - return True - else: - return False diff --git a/kb2/__init__.py b/kb2/__init__.py deleted file mode 100644 index 9786302c..00000000 --- a/kb2/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import ext diff --git a/kb2/ext/__init__.py b/kb2/ext/__init__.py deleted file mode 100644 index 18541b15..00000000 --- a/kb2/ext/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import koala diff --git a/kb2/ext/koala/__init__.py b/kb2/ext/koala/__init__.py deleted file mode 100644 index 9f215bda..00000000 --- a/kb2/ext/koala/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import commands diff --git a/kb2/ext/koala/api.py b/kb2/ext/koala/api.py deleted file mode 100644 index e69de29b..00000000 diff --git a/kb2/ext/koala/commands.py b/kb2/ext/koala/commands.py deleted file mode 100644 index 9a1231a4..00000000 --- a/kb2/ext/koala/commands.py +++ /dev/null @@ -1,15 +0,0 @@ -from dislord import CommandGroup -from dislord.discord.interactions.receiving_and_responding.interaction import Interaction -from dislord.discord.interactions.receiving_and_responding.interaction_response import InteractionResponse -from kb2.main import client - -koala_group = CommandGroup(name="koala", description="KoalaBot Base Commands") - - -@koala_group.command(name="support", description="KoalaBot Support server link") -def support(interaction: Interaction): - return InteractionResponse.message( - content="Join our support server for more help! https://discord.gg/5etEjVd") - - -client.register_group(koala_group) diff --git a/kb2/ext/koala/core.py b/kb2/ext/koala/core.py deleted file mode 100644 index 9b48f21f..00000000 --- a/kb2/ext/koala/core.py +++ /dev/null @@ -1,4 +0,0 @@ - - -def support(): - pass \ No newline at end of file diff --git a/kb2/ext/verify/__init__.py b/kb2/ext/verify/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/kb2/ext/verify/commands.py b/kb2/ext/verify/commands.py deleted file mode 100644 index 016b71ef..00000000 --- a/kb2/ext/verify/commands.py +++ /dev/null @@ -1,5 +0,0 @@ -from dislord import CommandGroup -from kb2.main import client - -verify_group = CommandGroup(name="verify", description="Configure Guild Verification") -client.register_group(verify_group) diff --git a/kb2/ext/verifyme/__init__.py b/kb2/ext/verifyme/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/kb2/ext/verifyme/commands.py b/kb2/ext/verifyme/commands.py deleted file mode 100644 index 45eb2133..00000000 --- a/kb2/ext/verifyme/commands.py +++ /dev/null @@ -1,11 +0,0 @@ -from dislord.discord.interactions.components.enums import ButtonStyle -from dislord.discord.interactions.components.models import Component, ActionRow, Button -from dislord.discord.interactions.receiving_and_responding.interaction_response import InteractionResponse -from kb2.main import client -from dislord.discord.interactions.receiving_and_responding.interaction import Interaction - - -@client.command(name="verifyme", description="Verify your identity with Koala") -def verify_me(interaction: Interaction): - return InteractionResponse.message(content="Please verify yourself through the below link", components=[ActionRow( - components=[Button(style=ButtonStyle.LINK, label="Verify", custom_id="1", url="https://koalabot.uk")])]) diff --git a/kb2/main.py b/kb2/main.py deleted file mode 100644 index 04a55a25..00000000 --- a/kb2/main.py +++ /dev/null @@ -1,23 +0,0 @@ -import os - -import dislord - -PUBLIC_KEY = os.environ.get("PUBLIC_KEY") -BOT_TOKEN = os.environ.get("DISCORD_TOKEN") - -client = dislord.ApplicationClient(PUBLIC_KEY, BOT_TOKEN) - -def serverless_handler(event, context): # Not needed if using server - return client.serverless_handler(event, context) - - -def sync_serverless_handler(event, context): - client.sync_commands() - client.sync_commands(guild_ids=[g.id for g in client.guilds]) - return {"statusCode": 200} - - -if __name__ == '__main__': # Not needed if using serverless - client.sync_commands() - client.sync_commands(guild_ids=[g.id for g in client.guilds]) - dislord.server.start_server(client, host='0.0.0.0', debug=True, port=8123) 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 c2fccc99..5d213f93 100644 --- a/koala/cogs/base/cog.py +++ b/koala/cogs/base/cog.py @@ -21,6 +21,7 @@ from . import core from .log import logger from .utils import AUTO_UPDATE_ACTIVITY_DELAY +from ... import env # Constants @@ -228,11 +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)) + 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 ec0cdc92..bbe33491 100644 --- a/koala/cogs/react_for_role/cog.py +++ b/koala/cogs/react_for_role/cog.py @@ -29,20 +29,11 @@ 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): @@ -56,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): """ @@ -64,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 @@ -121,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): """ @@ -132,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 " @@ -193,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): """ @@ -203,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) @@ -218,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): """ @@ -228,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) @@ -243,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): """ @@ -253,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) @@ -268,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): """ @@ -278,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) @@ -306,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): """ @@ -317,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": @@ -367,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) @@ -411,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): """ @@ -425,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 " @@ -470,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): """ @@ -485,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.") @@ -588,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): """ @@ -600,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): """ @@ -618,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( @@ -625,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): """ @@ -635,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: 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/cog.py b/koala/cogs/verification/cog.py index 8abf5906..30cf2301 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) @@ -138,7 +134,7 @@ async def blacklist_list(self, ctx): await ctx.send(embed=embed) - @commands.check(koalabot.is_dm_channel) + @commands.check(is_enabled) @commands.command(name="verify") async def verify(self, ctx, email: str): """ @@ -200,13 +196,14 @@ async def get_emails(self, ctx, user_id: int): await ctx.send(f"This user has registered with:\n{emails}") @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 +212,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 +222,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/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..5497b18f --- /dev/null +++ b/koala/kb2/adapter.py @@ -0,0 +1,58 @@ +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): + 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: + 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): + 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..331d07eb --- /dev/null +++ b/koala/kb2/models.py @@ -0,0 +1,32 @@ +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 390fe4e6..6c380635 100644 --- a/koalabot.py +++ b/koalabot.py @@ -18,27 +18,22 @@ # Built-in/Generic Imports import asyncio import time -from http.client import OK from typing import Any +import aiohttp_cors import discord # Libs from aiohttp import web -import aiohttp_cors -from discord import Interaction from discord.ext import commands -from discord.http import Route -from discord.webhook.async_ import async_context -from dislord.discord.interactions.receiving_and_responding.interaction_response import InteractionCallbackType 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 -import kb2.main # Constants COMMAND_PREFIX = "k!" @@ -63,6 +58,8 @@ intent.message_content = True is_dpytest = False +check_error: [int, str] = {} + class KoalaBot(commands.Bot): """ @@ -72,22 +69,12 @@ def parse_interaction_create(self, data: dict) -> None: self.dispatch('raw_interaction', data) async def on_raw_interaction(self, raw_interaction: dict): - interaction = discord.Interaction(data=raw_interaction, state=self._connection) - result = kb2.main.client.interact(raw_interaction) - if result is None or result.status_code != OK: - logger.error("Failed to process interaction: %s", interaction) - return - route = Route( - 'POST', - '/interactions/{webhook_id}/{webhook_token}/callback', - webhook_id=interaction.id, - webhook_token=interaction.token, - ) - await async_context.get().request(route, session=interaction.response._parent._session, payload=result.body) + 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: guild_id = "UNKNOWN" @@ -126,8 +113,8 @@ async def setup_hook(self) -> None: """ 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]) + # 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 @@ -162,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): @@ -201,18 +190,42 @@ 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 ctx is not None: + channel = ctx.channel + 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 c372ab64..62608c11 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,15 +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==2.0.7 wcwidth==0.2.6 websockets==10.4 yarl==1.8.2 discord-interactions~=0.4.0 -requests~=2.31.0 +pynamodb~=6.0 diff --git a/tests/kb2/__init__.py b/tests/kb2/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/kb2/ext/__init__.py b/tests/kb2/ext/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/kb2/ext/koala/__init__.py b/tests/kb2/ext/koala/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/kb2/ext/koala/test_commands.py b/tests/kb2/ext/koala/test_commands.py deleted file mode 100644 index 11e9238d..00000000 --- a/tests/kb2/ext/koala/test_commands.py +++ /dev/null @@ -1,8 +0,0 @@ -from dislord.discord.interactions.receiving_and_responding.interaction_response import InteractionCallbackType -from kb2.ext.koala import commands - - -def test_support(): - response = commands.support(None) - assert response.type == InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE - assert response.data.content == "Join our support server for more help! https://discord.gg/5etEjVd" 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 From 5b43e0c1ce8b196e2a05640728b7a7a32b2bef06 Mon Sep 17 00:00:00 2001 From: Jack Draper <drapj002@gmail.com> Date: Sun, 27 Oct 2024 11:06:54 +0000 Subject: [PATCH 11/16] fix: downgrade urllib3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 62608c11..459114fc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,7 +32,7 @@ six==1.16.0 sqlalchemy==1.4.37 toml==0.10.2 twitchAPI==3.9.0 -urllib3==2.0.7 +urllib3~=1.24 wcwidth==0.2.6 websockets==10.4 yarl==1.8.2 From f9128d58071ccc4c608b8f1baeb9b1fd51238490 Mon Sep 17 00:00:00 2001 From: Jack Draper <drapj002@gmail.com> Date: Sun, 27 Oct 2024 11:09:52 +0000 Subject: [PATCH 12/16] chore: downgrade python version use of subscriptable type --- koala/kb2/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/koala/kb2/models.py b/koala/kb2/models.py index 331d07eb..60ac03b9 100644 --- a/koala/kb2/models.py +++ b/koala/kb2/models.py @@ -1,3 +1,5 @@ +from typing import List + from pynamodb.attributes import NumberAttribute, BooleanAttribute, UnicodeAttribute, MapAttribute, ListAttribute from pynamodb.models import Model @@ -17,7 +19,7 @@ class Meta: region = 'eu-west-2' guild_id: str = UnicodeAttribute(hash_key=True) - extensions: list[ExtensionAttr] = ListAttribute(of=ExtensionAttr, default=list) + extensions: List[ExtensionAttr] = ListAttribute(of=ExtensionAttr, default=list) map_ext = { From 2a2d037c92d7df209ea1d7f3300c255149825dbb Mon Sep 17 00:00:00 2001 From: Jack Draper <drapj002@gmail.com> Date: Sun, 27 Oct 2024 22:06:42 +0000 Subject: [PATCH 13/16] test: fix tests --- koalabot.py | 4 +++- tests/koala/cogs/twitch_alert/test_cog.py | 12 ++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/koalabot.py b/koalabot.py index 6c380635..37e62bed 100644 --- a/koalabot.py +++ b/koalabot.py @@ -197,8 +197,9 @@ def check_guild_has_ext(ctx=None, extension_id=None, channel=None, guild_id=None :param extension_id: The koala extension ID :return: True if has ext """ - if ctx is not None: + 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: @@ -214,6 +215,7 @@ def check_guild_has_ext(ctx=None, extension_id=None, channel=None, guild_id=None else: return None + def ext_enabled_func(extension_id): return lambda ctx: check_guild_has_ext(ctx, extension_id) is None diff --git a/tests/koala/cogs/twitch_alert/test_cog.py b/tests/koala/cogs/twitch_alert/test_cog.py index 25b16bac..a675bc51 100644 --- a/tests/koala/cogs/twitch_alert/test_cog.py +++ b/tests/koala/cogs/twitch_alert/test_cog.py @@ -19,6 +19,8 @@ from koala.cogs.twitch_alert.models import UserInTwitchAlert from koala.colours import KOALA_GREEN from koala.db import session_manager +from koala.kb2.models import Guild, ExtensionAttr +from koala.models import Guilds from tests.koala.tests_utils.last_ctx_cog import LastCtxCog # Constants @@ -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)) From b097c82fb82466c7477c71195c30ee8641b9fa01 Mon Sep 17 00:00:00 2001 From: Jack Draper <drapj002@gmail.com> Date: Sun, 27 Oct 2024 22:13:57 +0000 Subject: [PATCH 14/16] fix: incorrect checks on verify --- koala/cogs/verification/cog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/koala/cogs/verification/cog.py b/koala/cogs/verification/cog.py index 30cf2301..ab7ff900 100644 --- a/koala/cogs/verification/cog.py +++ b/koala/cogs/verification/cog.py @@ -134,7 +134,7 @@ async def blacklist_list(self, ctx): await ctx.send(embed=embed) - @commands.check(is_enabled) + @commands.check(koalabot.is_dm_channel) @commands.command(name="verify") async def verify(self, ctx, email: str): """ @@ -195,6 +195,7 @@ 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"]) async def check_verifications(self, ctx): """ From d88a0786987b06f3a18a45bdb2180f1ae5565f0c Mon Sep 17 00:00:00 2001 From: Jack Draper <drapj002@gmail.com> Date: Sun, 27 Oct 2024 22:30:46 +0000 Subject: [PATCH 15/16] chore: add Restructured Text --- koala/kb2/adapter.py | 16 ++++++++++++++++ koala/kb2/models.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/koala/kb2/adapter.py b/koala/kb2/adapter.py index 5497b18f..15496f37 100644 --- a/koala/kb2/adapter.py +++ b/koala/kb2/adapter.py @@ -18,6 +18,9 @@ class AutherOAuthToken: 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"}, @@ -32,6 +35,13 @@ def get_headers(self) -> dict: @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 @@ -45,6 +55,12 @@ class KB2Adapter: 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', diff --git a/koala/kb2/models.py b/koala/kb2/models.py index 60ac03b9..1b0d0178 100644 --- a/koala/kb2/models.py +++ b/koala/kb2/models.py @@ -7,6 +7,7 @@ error_versions = {} + class ExtensionAttr(MapAttribute): id: str = UnicodeAttribute(hash_key=True) version: int = NumberAttribute() @@ -31,4 +32,3 @@ class Meta: "Verify": "verify", "Vote": "vote" } - From 3e6c9c49086a845234a991ce509d6c38b956a984 Mon Sep 17 00:00:00 2001 From: Jack Draper <drapj002@gmail.com> Date: Sun, 27 Oct 2024 22:32:50 +0000 Subject: [PATCH 16/16] chore: add changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) 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