From ad8ae2e10e1826a3b089992323e6869f312e4712 Mon Sep 17 00:00:00 2001 From: skelmis Date: Sat, 24 Feb 2024 22:25:13 +1300 Subject: [PATCH 01/12] fix: InteractionHandler tests --- suggestions/cogs/suggestion_cog.py | 4 +-- suggestions/core/__init__.py | 0 suggestions/interaction_handler.py | 9 ++++--- tests/conftest.py | 6 ++++- tests/mocks/database.py | 2 +- tests/test_interaction_handler.py | 42 ++++++++++++++++++------------ 6 files changed, 39 insertions(+), 24 deletions(-) create mode 100644 suggestions/core/__init__.py diff --git a/suggestions/cogs/suggestion_cog.py b/suggestions/cogs/suggestion_cog.py index 190185d..e12ba75 100644 --- a/suggestions/cogs/suggestion_cog.py +++ b/suggestions/cogs/suggestion_cog.py @@ -1,13 +1,13 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Optional, cast +from typing import TYPE_CHECKING, Optional import cooldowns import disnake from commons.caching import NonExistentEntry from bot_base.wraps import WrappedChannel -from disnake import Guild, Localized +from disnake import Guild from disnake.ext import commands, components from suggestions import checks, Stats, ErrorCode diff --git a/suggestions/core/__init__.py b/suggestions/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/suggestions/interaction_handler.py b/suggestions/interaction_handler.py index 34bcea9..3f4023c 100644 --- a/suggestions/interaction_handler.py +++ b/suggestions/interaction_handler.py @@ -1,9 +1,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import cast -if TYPE_CHECKING: - from suggestions import SuggestionsBot +import disnake + +from suggestions import SuggestionsBot class InteractionHandler: @@ -56,7 +57,6 @@ async def send( async def new_handler( cls, interaction: disnake.Interaction, - bot: SuggestionsBot, *, ephemeral: bool = True, with_message: bool = True, @@ -68,6 +68,7 @@ async def new_handler( # Register this on the bot instance so other areas can # request the interaction handler, such as error handlers + bot = cast(SuggestionsBot, interaction.client) bot.state.interaction_handlers.add_entry(interaction.application_id, instance) return instance diff --git a/tests/conftest.py b/tests/conftest.py index 8c627f7..c95f42e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,7 +16,7 @@ async def mocked_database() -> MockedSuggestionsMongoManager: @pytest.fixture -async def bot(monkeypatch): +async def bot(monkeypatch, mocked_database): if "./suggestions" not in [x[0] for x in os.walk(".")]: monkeypatch.chdir("..") @@ -48,3 +48,7 @@ async def injection_metadata(causar: Causar) -> InjectionMetadata: guild_id=881118111967883295, channel_id=causar.faker.generate_snowflake() ) + +@pytest.fixture +async def interaction_handler(bot) -> InteractionHandler: + return InteractionHandler(AsyncMock(), True, True) diff --git a/tests/mocks/database.py b/tests/mocks/database.py index 7336a03..f47527e 100644 --- a/tests/mocks/database.py +++ b/tests/mocks/database.py @@ -13,7 +13,7 @@ class MockedSuggestionsMongoManager: def __init__(self): - self.database_name = "suggestions-rewrite" + self.database_name = "suggestions-rewrite-testing" self.__mongo = AsyncMongoMockClient() self.db = self.__mongo[self.database_name] diff --git a/tests/test_interaction_handler.py b/tests/test_interaction_handler.py index 681b077..daeb512 100644 --- a/tests/test_interaction_handler.py +++ b/tests/test_interaction_handler.py @@ -1,8 +1,9 @@ -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, call import pytest -from bot_base import NonExistentEntry +from commons.caching import NonExistentEntry +from suggestions import SuggestionsBot from suggestions.interaction_handler import InteractionHandler @@ -14,32 +15,40 @@ async def test_send(interaction_handler: InteractionHandler): await interaction_handler.send("Hello world") assert interaction_handler.has_sent_something is True - assert interaction_handler.interaction.send.assert_called_with( - content="Hello world", ephemeral=True - ) + assert interaction_handler.interaction.send.call_count == 1 + assert interaction_handler.interaction.send.mock_calls == [ + call.send(ephemeral=True, content="Hello world") + ] interaction_handler.interaction = AsyncMock() await interaction_handler.send( "Hello world", embed="Embed", file="File", components=["Test"] ) - assert interaction_handler.interaction.send.assert_called_with( - content="Hello world", - ephemeral=True, - embed="Embed", - file="File", - components=["Test"], - ) + assert interaction_handler.interaction.send.call_count == 1 + assert interaction_handler.interaction.send.mock_calls == [ + call.send( + content="Hello world", + ephemeral=True, + embed="Embed", + file="File", + components=["Test"], + ), + ] -async def test_new_handler(bot): +async def test_new_handler(bot: SuggestionsBot): + mock = AsyncMock() + mock.client = bot + mock.application_id = 1 assert bot.state.interaction_handlers.cache == {} - handler: InteractionHandler = await InteractionHandler.new_handler(AsyncMock(), bot) + handler: InteractionHandler = await InteractionHandler.new_handler(mock) assert bot.state.interaction_handlers.cache != {} assert handler.has_sent_something is False assert handler.is_deferred is True + mock.application_id = 2 handler_2: InteractionHandler = await InteractionHandler.new_handler( - AsyncMock(), bot, ephemeral=False, with_message=False + mock, ephemeral=False, with_message=False ) assert handler_2.ephemeral is False assert handler_2.with_message is False @@ -51,8 +60,9 @@ async def test_fetch_handler(bot): await InteractionHandler.fetch_handler(application_id, bot) mock = AsyncMock() + mock.client = bot mock.application_id = application_id - await InteractionHandler.new_handler(mock, bot, with_message=False) + await InteractionHandler.new_handler(mock, with_message=False) handler: InteractionHandler = await InteractionHandler.fetch_handler( application_id, bot ) From 6fc421e4e786b2e9b603fe48219173df60f36f30 Mon Sep 17 00:00:00 2001 From: skelmis Date: Sat, 24 Feb 2024 22:50:59 +1300 Subject: [PATCH 02/12] feat: move current suggestions queue to use interaction handler --- suggestions/bot.py | 7 +- suggestions/cogs/suggestion_queue_cog.py | 243 ++------------------- suggestions/core/__init__.py | 1 + suggestions/core/suggestions_queue.py | 255 +++++++++++++++++++++++ suggestions/exceptions.py | 4 + suggestions/interaction_handler.py | 36 +++- tests/test_interaction_handler.py | 14 +- 7 files changed, 323 insertions(+), 237 deletions(-) create mode 100644 suggestions/core/suggestions_queue.py diff --git a/suggestions/bot.py b/suggestions/bot.py index fad449b..2473e7f 100644 --- a/suggestions/bot.py +++ b/suggestions/bot.py @@ -38,6 +38,7 @@ PartialResponse, ) from suggestions.http_error_parser import try_parse_http_error +from suggestions.interaction_handler import InteractionHandler from suggestions.objects import Error, GuildConfig, UserConfig from suggestions.stats import Stats, StatsEnum from suggestions.database import SuggestionsMongoManager @@ -830,11 +831,15 @@ def inject_locale_values( def get_localized_string( self, key: str, - interaction: disnake.Interaction, + interaction: disnake.Interaction | InteractionHandler, *, extras: Optional[dict] = None, guild_config: Optional[GuildConfig] = None, ): + if isinstance(interaction, InteractionHandler): + # Support this so easier going forward + interaction = interaction.interaction + content = self.get_locale(key, interaction.locale) return self.inject_locale_values( content, interaction=interaction, guild_config=guild_config, extras=extras diff --git a/suggestions/cogs/suggestion_queue_cog.py b/suggestions/cogs/suggestion_queue_cog.py index 5f29c86..b93d310 100644 --- a/suggestions/cogs/suggestion_queue_cog.py +++ b/suggestions/cogs/suggestion_queue_cog.py @@ -16,7 +16,9 @@ from suggestions import checks from suggestions.cooldown_bucket import InteractionBucket +from suggestions.core import SuggestionsQueue from suggestions.exceptions import ErrorHandled +from suggestions.interaction_handler import InteractionHandler from suggestions.objects import GuildConfig, UserConfig, QueuedSuggestion from suggestions.qs_paginator import QueuedSuggestionsPaginator @@ -30,167 +32,30 @@ class SuggestionsQueueCog(commands.Cog): def __init__(self, bot): - self.bot: SuggestionsBot = bot - self.state = self.bot.state - self.queued_suggestions_db: Document = self.bot.db.queued_suggestions - self.paginator_objects: TimedCache = TimedCache( - global_ttl=timedelta(minutes=15), - lazy_eviction=False, - ttl_from_last_access=True, - ) - - async def get_paginator_for( - self, paginator_id: str, interaction: disnake.Interaction - ) -> QueuedSuggestionsPaginator: - try: - return self.paginator_objects.get_entry(paginator_id) - except NonExistentEntry: - await interaction.send( - self.bot.get_localized_string( - "PAGINATION_INNER_SESSION_EXPIRED", interaction - ), - ephemeral=True, - ) - raise ErrorHandled + self.bot = bot + self.core: SuggestionsQueue = SuggestionsQueue(bot) @components.button_listener() async def next_button(self, inter: disnake.MessageInteraction, *, pid: str): - await inter.response.defer(ephemeral=True, with_message=True) - paginator = await self.get_paginator_for(pid, inter) - paginator.current_page += 1 - await paginator.original_interaction.edit_original_message( - embed=await paginator.format_page() - ) - await inter.send( - self.bot.get_localized_string("PAGINATION_INNER_NEXT_ITEM", inter), - ephemeral=True, - ) + await self.core.next_button(await InteractionHandler.new_handler(inter), pid) @components.button_listener() async def previous_button(self, inter: disnake.MessageInteraction, *, pid: str): - await inter.response.defer(ephemeral=True, with_message=True) - paginator = await self.get_paginator_for(pid, inter) - paginator.current_page -= 1 - await paginator.original_interaction.edit_original_message( - embed=await paginator.format_page() - ) - await inter.send( - self.bot.get_localized_string("PAGINATION_INNER_PREVIOUS_ITEM", inter), - ephemeral=True, + await self.core.previous_button( + await InteractionHandler.new_handler(inter), pid ) @components.button_listener() async def stop_button(self, inter: disnake.MessageInteraction, *, pid: str): - await inter.response.defer(ephemeral=True, with_message=True) - paginator = await self.get_paginator_for(pid, inter) - self.paginator_objects.delete_entry(pid) - await paginator.original_interaction.edit_original_message( - components=[], - embeds=[], - content=self.bot.get_localized_string( - "PAGINATION_INNER_QUEUE_EXPIRED", inter - ), - ) - await inter.send( - self.bot.get_localized_string("PAGINATION_INNER_QUEUE_CANCELLED", inter), - ephemeral=True, - ) + await self.core.stop_button(await InteractionHandler.new_handler(inter), pid) @components.button_listener() async def approve_button(self, inter: disnake.MessageInteraction, *, pid: str): - await inter.response.defer(ephemeral=True, with_message=True) - paginator = await self.get_paginator_for(pid, inter) - current_suggestion: QueuedSuggestion = ( - await paginator.get_current_queued_suggestion() - ) - suggestion: None = None - try: - await paginator.remove_current_page() - suggestion: Suggestion = await current_suggestion.resolve( - was_approved=True, state=self.bot.state, resolved_by=inter.author.id - ) - guild_config: GuildConfig = await GuildConfig.from_id( - inter.guild_id, self.state - ) - icon_url = await Guild.try_fetch_icon_url(inter.guild_id, self.state) - guild = self.state.guild_cache.get_entry(inter.guild_id) - await suggestion.setup_initial_messages( - guild_config=guild_config, - interaction=inter, - state=self.state, - bot=self.bot, - cog=self.bot.get_cog("SuggestionsCog"), - guild=guild, - icon_url=icon_url, - comes_from_queue=True, - ) - except: - # Throw it back in the queue on error - current_suggestion.resolved_by = None - current_suggestion.resolved_at = None - current_suggestion.still_in_queue = True - await self.bot.state.queued_suggestions_db.update( - current_suggestion, current_suggestion - ) - - if suggestion is not None: - await self.bot.state.suggestions_db.delete(suggestion) - - raise - - await inter.send( - self.bot.get_localized_string("PAGINATION_INNER_QUEUE_ACCEPTED", inter), - ephemeral=True, - ) + await self.core.approve_button(await InteractionHandler.new_handler(inter), pid) @components.button_listener() async def reject_button(self, inter: disnake.MessageInteraction, *, pid: str): - await inter.response.defer(ephemeral=True, with_message=True) - paginator = await self.get_paginator_for(pid, inter) - current_suggestion = await paginator.get_current_queued_suggestion() - await paginator.remove_current_page() - await current_suggestion.resolve( - was_approved=False, state=self.bot.state, resolved_by=inter.author.id - ) - try: - guild_config: GuildConfig = await GuildConfig.from_id( - inter.guild_id, self.state - ) - icon_url = await Guild.try_fetch_icon_url(inter.guild_id, self.state) - guild = self.state.guild_cache.get_entry(inter.guild_id) - embed: disnake.Embed = disnake.Embed( - description=self.bot.get_localized_string( - "QUEUE_INNER_USER_REJECTED", inter - ), - colour=self.bot.colors.embed_color, - timestamp=self.state.now, - ) - embed.set_author( - name=guild.name, - icon_url=icon_url, - ) - embed.set_footer(text=f"Guild ID {inter.guild_id}") - user = await self.bot.get_or_fetch_user( - current_suggestion.suggestion_author_id - ) - user_config: UserConfig = await UserConfig.from_id( - current_suggestion.suggestion_author_id, self.bot.state - ) - if ( - not user_config.dm_messages_disabled - and not guild_config.dm_messages_disabled - ): - await user.send(embed=embed) - except disnake.HTTPException: - log.debug( - "Failed to DM %s regarding their queued suggestion", - current_suggestion.suggestion_author_id, - ) - - await inter.send( - self.bot.get_localized_string("PAGINATION_INNER_QUEUE_REJECTED", inter), - ephemeral=True, - ) + await self.core.reject_button(await InteractionHandler.new_handler(inter), pid) @commands.slash_command( dm_permission=False, @@ -204,91 +69,19 @@ async def queue(self, interaction: disnake.GuildCommandInteraction): @queue.sub_command() async def info(self, interaction: disnake.GuildCommandInteraction): """View information about this guilds suggestions queue.""" - await interaction.response.defer(ephemeral=True, with_message=True) - guild_config: GuildConfig = await GuildConfig.from_id( - interaction.guild_id, self.state - ) - count: int = await self.queued_suggestions_db.count( - AQ(AND(EQ("guild_id", interaction.guild_id), EQ("still_in_queue", True))) - ) - icon_url = await Guild.try_fetch_icon_url(interaction.guild_id, self.state) - guild = self.state.guild_cache.get_entry(interaction.guild_id) - embed = disnake.Embed( - title="Queue Info", - timestamp=self.bot.state.now, - description=f"`{count}` suggestions currently in queue.\n" - f"New suggestions will {'' if guild_config.uses_suggestion_queue else 'not'} be " - f"sent to the suggestions queue.", - colour=self.bot.colors.embed_color, - ) - embed.set_author( - name=guild.name, - icon_url=icon_url, - ) - await interaction.send(embed=embed, ephemeral=True) + await self.core.info(await InteractionHandler.new_handler(interaction)) @queue.sub_command() async def view(self, interaction: disnake.GuildCommandInteraction): """View this guilds suggestions queue.""" - await interaction.response.defer(ephemeral=True, with_message=True) - guild_config: GuildConfig = await GuildConfig.from_id( - interaction.guild_id, self.state - ) - data: list = await self.queued_suggestions_db.find_many( - AQ(AND(EQ("guild_id", interaction.guild_id), EQ("still_in_queue", True))), - projections=Projection(SHOW("_id")), - try_convert=False, - ) - if not data: - return await interaction.send( - self.bot.get_localized_string( - "QUEUE_VIEW_INNER_NOTHING_QUEUED", interaction - ), - ephemeral=True, - ) - - content = None - if not guild_config.uses_suggestion_queue: - content = self.bot.get_localized_string( - "QUEUE_VIEW_INNER_PRIOR_QUEUE", interaction - ) - - paginator = QueuedSuggestionsPaginator( - bot=self.bot, data=[d["_id"] for d in data], inter=interaction - ) - pid = self.bot.state.get_new_sq_paginator_id() - await interaction.send( - content=content, - ephemeral=True, - embed=await paginator.format_page(), - components=[ - disnake.ui.ActionRow( - disnake.ui.Button( - emoji="\N{BLACK LEFT-POINTING TRIANGLE}\ufe0f", - custom_id=await self.previous_button.build_custom_id(pid=pid), - ), - disnake.ui.Button( - emoji="\N{BLACK SQUARE FOR STOP}\ufe0f", - custom_id=await self.stop_button.build_custom_id(pid=pid), - ), - disnake.ui.Button( - emoji="\N{BLACK RIGHT-POINTING TRIANGLE}\ufe0f", - custom_id=await self.next_button.build_custom_id(pid=pid), - ), - ), - disnake.ui.ActionRow( - disnake.ui.Button( - emoji=await self.bot.suggestion_emojis.default_up_vote(), - custom_id=await self.approve_button.build_custom_id(pid=pid), - ), - disnake.ui.Button( - emoji=await self.bot.suggestion_emojis.default_down_vote(), - custom_id=await self.reject_button.build_custom_id(pid=pid), - ), - ), - ], + await self.core.view( + await InteractionHandler.new_handler(interaction), + self.previous_button, + self.next_button, + self.stop_button, + self.approve_button, + self.reject_button, ) - self.paginator_objects.add_entry(pid, paginator) def setup(bot): diff --git a/suggestions/core/__init__.py b/suggestions/core/__init__.py index e69de29..035c03d 100644 --- a/suggestions/core/__init__.py +++ b/suggestions/core/__init__.py @@ -0,0 +1 @@ +from .suggestions_queue import SuggestionsQueue diff --git a/suggestions/core/suggestions_queue.py b/suggestions/core/suggestions_queue.py new file mode 100644 index 0000000..d11edac --- /dev/null +++ b/suggestions/core/suggestions_queue.py @@ -0,0 +1,255 @@ +from __future__ import annotations + +import logging +from datetime import timedelta +from typing import TYPE_CHECKING + + +import disnake +from alaric import AQ +from alaric.comparison import EQ +from alaric.logical import AND +from alaric.projections import Projection, SHOW +from commons.caching import NonExistentEntry, TimedCache +from disnake import Guild + +from suggestions.exceptions import ErrorHandled +from suggestions.interaction_handler import InteractionHandler +from suggestions.objects import GuildConfig, UserConfig, QueuedSuggestion +from suggestions.qs_paginator import QueuedSuggestionsPaginator + +if TYPE_CHECKING: + from alaric import Document + from suggestions import SuggestionsBot + from suggestions.objects import Suggestion + +log = logging.getLogger(__name__) + + +class SuggestionsQueue: + def __init__(self, bot): + self.bot: SuggestionsBot = bot + self.paginator_objects: TimedCache = TimedCache( + global_ttl=timedelta(minutes=15), + lazy_eviction=False, + ttl_from_last_access=True, + ) + + @property + def queued_suggestions_db(self) -> Document: + return self.bot.db.queued_suggestions + + @property + def state(self): + return self.bot.state + + async def get_paginator_for( + self, paginator_id: str, ih: InteractionHandler + ) -> QueuedSuggestionsPaginator: + try: + return self.paginator_objects.get_entry(paginator_id) + except NonExistentEntry: + await ih.send(translation_key="PAGINATION_INNER_SESSION_EXPIRED") + raise ErrorHandled + + async def next_button(self, ih: InteractionHandler, pid: str): + paginator = await self.get_paginator_for(pid, ih) + paginator.current_page += 1 + await paginator.original_interaction.edit_original_message( + embed=await paginator.format_page() + ) + await ih.send(translation_key="PAGINATION_INNER_NEXT_ITEM") + + async def previous_button(self, ih: InteractionHandler, pid: str): + paginator = await self.get_paginator_for(pid, ih) + paginator.current_page -= 1 + await paginator.original_interaction.edit_original_message( + embed=await paginator.format_page() + ) + await ih.send(translation_key="PAGINATION_INNER_PREVIOUS_ITEM") + + async def stop_button(self, ih: InteractionHandler, pid: str): + paginator = await self.get_paginator_for(pid, ih) + self.paginator_objects.delete_entry(pid) + await paginator.original_interaction.edit_original_message( + components=[], + embeds=[], + content=self.bot.get_localized_string( + "PAGINATION_INNER_QUEUE_EXPIRED", ih.interaction + ), + ) + await ih.send(translation_key="PAGINATION_INNER_QUEUE_CANCELLED") + + async def approve_button(self, ih: InteractionHandler, pid: str): + paginator = await self.get_paginator_for(pid, ih) + current_suggestion: QueuedSuggestion = ( + await paginator.get_current_queued_suggestion() + ) + suggestion: None = None + guild_id = ih.interaction.guild_id + try: + await paginator.remove_current_page() + suggestion: Suggestion = await current_suggestion.resolve( + was_approved=True, + state=self.bot.state, + resolved_by=ih.interaction.author.id, + ) + guild_config: GuildConfig = await GuildConfig.from_id(guild_id, self.state) + icon_url = await Guild.try_fetch_icon_url(guild_id, self.state) + guild = self.state.guild_cache.get_entry(guild_id) + await suggestion.setup_initial_messages( + guild_config=guild_config, + interaction=ih.interaction, + state=self.state, + bot=self.bot, + cog=self.bot.get_cog("SuggestionsCog"), + guild=guild, + icon_url=icon_url, + comes_from_queue=True, + ) + except: + # Throw it back in the queue on error + current_suggestion.resolved_by = None + current_suggestion.resolved_at = None + current_suggestion.still_in_queue = True + await self.bot.state.queued_suggestions_db.update( + current_suggestion, current_suggestion + ) + + if suggestion is not None: + await self.bot.state.suggestions_db.delete(suggestion) + + raise + + await ih.send(translation_key="PAGINATION_INNER_QUEUE_ACCEPTED") + + async def reject_button(self, ih: InteractionHandler, pid: str): + paginator = await self.get_paginator_for(pid, ih) + current_suggestion = await paginator.get_current_queued_suggestion() + await paginator.remove_current_page() + await current_suggestion.resolve( + was_approved=False, + state=self.bot.state, + resolved_by=ih.interaction.author.id, + ) + + guild_id = ih.interaction.guild_id + try: + guild_config: GuildConfig = await GuildConfig.from_id(guild_id, self.state) + icon_url = await Guild.try_fetch_icon_url(guild_id, self.state) + guild = self.state.guild_cache.get_entry(guild_id) + embed: disnake.Embed = disnake.Embed( + description=self.bot.get_localized_string( + "QUEUE_INNER_USER_REJECTED", ih + ), + colour=self.bot.colors.embed_color, + timestamp=self.state.now, + ) + embed.set_author( + name=guild.name, + icon_url=icon_url, + ) + embed.set_footer(text=f"Guild ID {guild_id}") + user = await self.bot.get_or_fetch_user( + current_suggestion.suggestion_author_id + ) + user_config: UserConfig = await UserConfig.from_id( + current_suggestion.suggestion_author_id, self.bot.state + ) + if ( + not user_config.dm_messages_disabled + and not guild_config.dm_messages_disabled + ): + await user.send(embed=embed) + except disnake.HTTPException: + log.debug( + "Failed to DM %s regarding their queued suggestion", + current_suggestion.suggestion_author_id, + ) + + await ih.send(translation_key="PAGINATION_INNER_QUEUE_REJECTED") + + async def info(self, ih: InteractionHandler): + guild_id = ih.interaction.guild_id + guild_config: GuildConfig = await GuildConfig.from_id(guild_id, self.state) + count: int = await self.queued_suggestions_db.count( + AQ(AND(EQ("guild_id", guild_id), EQ("still_in_queue", True))) + ) + icon_url = await Guild.try_fetch_icon_url(guild_id, self.state) + guild = self.state.guild_cache.get_entry(guild_id) + embed = disnake.Embed( + title="Queue Info", + timestamp=self.bot.state.now, + description=f"`{count}` suggestions currently in queue.\n" + f"New suggestions will {'' if guild_config.uses_suggestion_queue else 'not'} be " + f"sent to the suggestions queue.", + colour=self.bot.colors.embed_color, + ) + embed.set_author( + name=guild.name, + icon_url=icon_url, + ) + await ih.send(embed=embed) + + async def view( + self, + ih: InteractionHandler, + previous_button, + next_button, + stop_button, + approve_button, + reject_button, + ): + """View this guilds suggestions queue.""" + guild_id = ih.interaction.guild_id + guild_config: GuildConfig = await GuildConfig.from_id(guild_id, self.state) + data: list = await self.queued_suggestions_db.find_many( + AQ(AND(EQ("guild_id", guild_id), EQ("still_in_queue", True))), + projections=Projection(SHOW("_id")), + try_convert=False, + ) + if not data: + return await ih.send(translation_key="QUEUE_VIEW_INNER_NOTHING_QUEUED") + + content = None + if not guild_config.uses_suggestion_queue: + content = self.bot.get_localized_string( + "QUEUE_VIEW_INNER_PRIOR_QUEUE", ih.interaction + ) + + paginator = QueuedSuggestionsPaginator( + bot=self.bot, data=[d["_id"] for d in data], inter=ih.interaction + ) + pid = self.bot.state.get_new_sq_paginator_id() + await ih.interaction.send( + content=content, + ephemeral=True, + embed=await paginator.format_page(), + components=[ + disnake.ui.ActionRow( + disnake.ui.Button( + emoji="\N{BLACK LEFT-POINTING TRIANGLE}\ufe0f", + custom_id=await previous_button.build_custom_id(pid=pid), + ), + disnake.ui.Button( + emoji="\N{BLACK SQUARE FOR STOP}\ufe0f", + custom_id=await stop_button.build_custom_id(pid=pid), + ), + disnake.ui.Button( + emoji="\N{BLACK RIGHT-POINTING TRIANGLE}\ufe0f", + custom_id=await next_button.build_custom_id(pid=pid), + ), + ), + disnake.ui.ActionRow( + disnake.ui.Button( + emoji=await self.bot.suggestion_emojis.default_up_vote(), + custom_id=await approve_button.build_custom_id(pid=pid), + ), + disnake.ui.Button( + emoji=await self.bot.suggestion_emojis.default_down_vote(), + custom_id=await reject_button.build_custom_id(pid=pid), + ), + ), + ], + ) + self.paginator_objects.add_entry(pid, paginator) diff --git a/suggestions/exceptions.py b/suggestions/exceptions.py index 6669a63..0ab460d 100644 --- a/suggestions/exceptions.py +++ b/suggestions/exceptions.py @@ -53,3 +53,7 @@ class BlocklistedUser(CheckFailure): class PartialResponse(Exception): """A garven route returned a partial response when we require a full response""" + + +class ConflictingHandlerInformation(disnake.DiscordException): + """Raised when an InteractionHandler class gets both content and a translation key""" diff --git a/suggestions/interaction_handler.py b/suggestions/interaction_handler.py index 3f4023c..d53ce43 100644 --- a/suggestions/interaction_handler.py +++ b/suggestions/interaction_handler.py @@ -1,10 +1,13 @@ from __future__ import annotations -from typing import cast +from typing import cast, TYPE_CHECKING import disnake -from suggestions import SuggestionsBot +from suggestions.exceptions import ConflictingHandlerInformation + +if TYPE_CHECKING: + from suggestions import SuggestionsBot class InteractionHandler: @@ -17,9 +20,14 @@ class InteractionHandler: """ def __init__( - self, interaction: disnake.Interaction, ephemeral: bool, with_message: bool + self, + interaction: disnake.Interaction | disnake.GuildCommandInteraction, + ephemeral: bool, + with_message: bool, ): - self.interaction: disnake.Interaction = interaction + self.interaction: disnake.Interaction | disnake.GuildCommandInteraction = ( + interaction + ) self.ephemeral: bool = ephemeral self.with_message: bool = with_message self.is_deferred: bool = False @@ -29,6 +37,10 @@ def __init__( # errors if we haven't yet sent anything self.has_sent_something: bool = False + @property + def bot(self) -> SuggestionsBot: + return self.interaction.client # type: ignore + async def send( self, content: str | None = None, @@ -36,7 +48,14 @@ async def send( embed: disnake.Embed | None = None, file: disnake.File | None = None, components: list | None = None, + translation_key: str | None = None, ): + if translation_key is not None: + if content is not None: + raise ConflictingHandlerInformation + + content = self.bot.get_localized_string(translation_key, self.interaction) + data = {} if content is not None: data["content"] = content @@ -50,8 +69,9 @@ async def send( if not data: raise ValueError("Expected at-least one value to send.") - await self.interaction.send(ephemeral=self.ephemeral, **data) + value = await self.interaction.send(ephemeral=self.ephemeral, **data) self.has_sent_something = True + return value @classmethod async def new_handler( @@ -68,8 +88,10 @@ async def new_handler( # Register this on the bot instance so other areas can # request the interaction handler, such as error handlers - bot = cast(SuggestionsBot, interaction.client) - bot.state.interaction_handlers.add_entry(interaction.application_id, instance) + bot = interaction.client + if TYPE_CHECKING: + bot = cast(SuggestionsBot, bot) + bot.state.interaction_handlers.add_entry(interaction.id, instance) return instance diff --git a/tests/test_interaction_handler.py b/tests/test_interaction_handler.py index daeb512..6c87ba8 100644 --- a/tests/test_interaction_handler.py +++ b/tests/test_interaction_handler.py @@ -4,6 +4,7 @@ from commons.caching import NonExistentEntry from suggestions import SuggestionsBot +from suggestions.exceptions import ConflictingHandlerInformation from suggestions.interaction_handler import InteractionHandler @@ -39,14 +40,14 @@ async def test_send(interaction_handler: InteractionHandler): async def test_new_handler(bot: SuggestionsBot): mock = AsyncMock() mock.client = bot - mock.application_id = 1 + mock.id = 1 assert bot.state.interaction_handlers.cache == {} handler: InteractionHandler = await InteractionHandler.new_handler(mock) assert bot.state.interaction_handlers.cache != {} assert handler.has_sent_something is False assert handler.is_deferred is True - mock.application_id = 2 + mock.id = 2 handler_2: InteractionHandler = await InteractionHandler.new_handler( mock, ephemeral=False, with_message=False ) @@ -54,17 +55,22 @@ async def test_new_handler(bot: SuggestionsBot): assert handler_2.with_message is False -async def test_fetch_handler(bot): +async def test_fetch_handler(bot: SuggestionsBot): application_id = 123456789 with pytest.raises(NonExistentEntry): await InteractionHandler.fetch_handler(application_id, bot) mock = AsyncMock() mock.client = bot - mock.application_id = application_id + mock.id = application_id await InteractionHandler.new_handler(mock, with_message=False) handler: InteractionHandler = await InteractionHandler.fetch_handler( application_id, bot ) assert handler.with_message is False assert handler.ephemeral is True + + +async def test_dual_raises(interaction_handler: InteractionHandler): + with pytest.raises(ConflictingHandlerInformation): + await interaction_handler.send("Test", translation_key="Blah") From 75ae89eb6676a20a81c76a02a88e9bf950f545d4 Mon Sep 17 00:00:00 2001 From: skelmis Date: Sun, 25 Feb 2024 17:06:36 +1300 Subject: [PATCH 03/12] feat: port existing queue rejection/approval to new generic method --- suggestions/bot.py | 16 +- suggestions/codes.py | 1 + suggestions/cogs/suggestion_queue_cog.py | 12 ++ suggestions/core/suggestions_queue.py | 227 +++++++++++++++-------- suggestions/exceptions.py | 4 + suggestions/interaction_handler.py | 6 +- suggestions/locales/en_GB.json | 2 +- suggestions/locales/en_US.json | 5 +- suggestions/objects/guild_config.py | 3 + suggestions/objects/queued_suggestion.py | 61 +++++- suggestions/state.py | 32 +++- 11 files changed, 287 insertions(+), 82 deletions(-) diff --git a/suggestions/bot.py b/suggestions/bot.py index 2473e7f..229b8bb 100644 --- a/suggestions/bot.py +++ b/suggestions/bot.py @@ -36,6 +36,7 @@ QueueImbalance, BlocklistedUser, PartialResponse, + MissingQueueLogsChannel, ) from suggestions.http_error_parser import try_parse_http_error from suggestions.interaction_handler import InteractionHandler @@ -380,6 +381,19 @@ async def on_slash_command_error( ephemeral=True, ) + elif isinstance(exception, MissingQueueLogsChannel): + return await interaction.send( + embed=self.error_embed( + "Missing Queue Logs Channel", + "This command requires a queue log channel to use.\n" + "Please contact an administrator and ask them to set one up " + "using the following command.\n`/config queue_logs`", + error_code=ErrorCode.MISSING_QUEUE_LOG_CHANNEL, + error=error, + ), + ephemeral=True, + ) + elif isinstance(exception, commands.MissingPermissions): perms = ",".join(i for i in exception.missing_permissions) return await interaction.send( @@ -791,7 +805,7 @@ def get_locale(self, key: str, locale: Locale) -> str: return values[str(locale)] except KeyError: # Default to known translations if not set - return values["en-GB"] + return values.get("en-GB", values["en-US"]) @staticmethod def inject_locale_values( diff --git a/suggestions/codes.py b/suggestions/codes.py index ef7e0d4..c9fb800 100644 --- a/suggestions/codes.py +++ b/suggestions/codes.py @@ -26,6 +26,7 @@ class ErrorCode(IntEnum): QUEUE_IMBALANCE = 20 MISSING_QUEUE_CHANNEL = 21 BLOCKLISTED_USER = 22 + MISSING_QUEUE_LOG_CHANNEL = 23 @classmethod def from_value(cls, value: int) -> ErrorCode: diff --git a/suggestions/cogs/suggestion_queue_cog.py b/suggestions/cogs/suggestion_queue_cog.py index b93d310..9d35d07 100644 --- a/suggestions/cogs/suggestion_queue_cog.py +++ b/suggestions/cogs/suggestion_queue_cog.py @@ -57,6 +57,18 @@ async def approve_button(self, inter: disnake.MessageInteraction, *, pid: str): async def reject_button(self, inter: disnake.MessageInteraction, *, pid: str): await self.core.reject_button(await InteractionHandler.new_handler(inter), pid) + @components.button_listener() + async def accept_queued_suggestion(self, inter: disnake.MessageInteraction): + if inter.message is None: + raise ValueError("Unhandled exception, expected a message") + + await self.core.accept_queued_suggestion( + await InteractionHandler.new_handler(inter), + inter.message.id, + inter.message.channel.id, + self.accept_queued_suggestion, + ) + @commands.slash_command( dm_permission=False, default_member_permissions=disnake.Permissions(manage_guild=True), diff --git a/suggestions/core/suggestions_queue.py b/suggestions/core/suggestions_queue.py index d11edac..f298f92 100644 --- a/suggestions/core/suggestions_queue.py +++ b/suggestions/core/suggestions_queue.py @@ -13,7 +13,7 @@ from commons.caching import NonExistentEntry, TimedCache from disnake import Guild -from suggestions.exceptions import ErrorHandled +from suggestions.exceptions import ErrorHandled, MissingQueueLogsChannel from suggestions.interaction_handler import InteractionHandler from suggestions.objects import GuildConfig, UserConfig, QueuedSuggestion from suggestions.qs_paginator import QueuedSuggestionsPaginator @@ -27,6 +27,15 @@ class SuggestionsQueue: + """ + Approach to suggestions queue. + + If it gets put in the virtual queue, it's always in said queue. + If its put in a channel, it's always in the channel. + Although we do track it under the same db table, this just + saves needing to transition everything between them + """ + def __init__(self, bot): self.bot: SuggestionsBot = bot self.paginator_objects: TimedCache = TimedCache( @@ -80,93 +89,117 @@ async def stop_button(self, ih: InteractionHandler, pid: str): ) await ih.send(translation_key="PAGINATION_INNER_QUEUE_CANCELLED") - async def approve_button(self, ih: InteractionHandler, pid: str): - paginator = await self.get_paginator_for(pid, ih) - current_suggestion: QueuedSuggestion = ( - await paginator.get_current_queued_suggestion() - ) - suggestion: None = None + async def resolve_queued_suggestion( + self, + ih: InteractionHandler, + queued_suggestion: QueuedSuggestion, + *, + was_approved: bool, + ): + """Resolve a queued item, doing all the relevant actions""" guild_id = ih.interaction.guild_id + suggestion: Suggestion | None = None try: - await paginator.remove_current_page() - suggestion: Suggestion = await current_suggestion.resolve( - was_approved=True, - state=self.bot.state, - resolved_by=ih.interaction.author.id, - ) + queued_suggestion.resolved_by = ih.interaction.author.id + queued_suggestion.resolved_at = self.bot.state.now + queued_suggestion.still_in_queue = False + guild_config: GuildConfig = await GuildConfig.from_id(guild_id, self.state) - icon_url = await Guild.try_fetch_icon_url(guild_id, self.state) - guild = self.state.guild_cache.get_entry(guild_id) - await suggestion.setup_initial_messages( - guild_config=guild_config, - interaction=ih.interaction, - state=self.state, - bot=self.bot, - cog=self.bot.get_cog("SuggestionsCog"), - guild=guild, - icon_url=icon_url, - comes_from_queue=True, - ) - except: - # Throw it back in the queue on error - current_suggestion.resolved_by = None - current_suggestion.resolved_at = None - current_suggestion.still_in_queue = True - await self.bot.state.queued_suggestions_db.update( - current_suggestion, current_suggestion - ) + # Send the message to the relevant channel if required + if was_approved: + # Send this message through to the guilds suggestion channel + suggestion = await queued_suggestion.convert_to_suggestion( + self.bot.state + ) + icon_url = await Guild.try_fetch_icon_url(guild_id, self.state) + guild = await self.state.fetch_guild(guild_id) + await suggestion.setup_initial_messages( + guild_config=guild_config, + interaction=ih.interaction, + state=self.state, + bot=self.bot, + cog=self.bot.get_cog("SuggestionsCog"), + guild=guild, + icon_url=icon_url, + comes_from_queue=True, + ) + # We dont send the user a message here because setup_initial_messages + # does this for us + + else: + # We may need to send this rejected suggestion to a logs channel + if guild_config.queued_log_channel_id: + embed: disnake.Embed = await queued_suggestion.as_embed(self.bot) + channel: disnake.TextChannel = await self.bot.state.fetch_channel( + guild_config.queued_log_channel_id + ) + try: + await channel.send(embed=embed) + except disnake.Forbidden as e: + raise MissingQueueLogsChannel from e + + # message the user the outcome + user = await self.bot.state.fetch_user( + queued_suggestion.suggestion_author_id + ) + user_config: UserConfig = await UserConfig.from_id( + queued_suggestion.suggestion_author_id, self.bot.state + ) + icon_url = await Guild.try_fetch_icon_url(guild_id, self.state) + guild = self.state.guild_cache.get_entry(guild_id) + if ( + user_config.dm_messages_disabled + or guild_config.dm_messages_disabled + ): + # Set up not to message users + return + + embed: disnake.Embed = disnake.Embed( + description=self.bot.get_localized_string( + "QUEUE_INNER_USER_REJECTED", ih + ), + colour=self.bot.colors.embed_color, + timestamp=self.state.now, + ) + embed.set_author( + name=guild.name, + icon_url=icon_url, + ) + embed.set_footer(text=f"Guild ID {guild_id}") + await user.send( + embeds=[embed, await queued_suggestion.as_embed(self.bot)] + ) + except: + # Don't remove from queue on failure if suggestion is not None: await self.bot.state.suggestions_db.delete(suggestion) + # Re-raise for the bot handler raise + else: + await self.bot.state.queued_suggestions_db.update( + queued_suggestion, queued_suggestion + ) + async def approve_button(self, ih: InteractionHandler, pid: str): + paginator = await self.get_paginator_for(pid, ih) + current_suggestion: QueuedSuggestion = ( + await paginator.get_current_queued_suggestion() + ) + await self.resolve_queued_suggestion( + ih, queued_suggestion=current_suggestion, was_approved=True + ) + await paginator.remove_current_page() await ih.send(translation_key="PAGINATION_INNER_QUEUE_ACCEPTED") async def reject_button(self, ih: InteractionHandler, pid: str): paginator = await self.get_paginator_for(pid, ih) current_suggestion = await paginator.get_current_queued_suggestion() - await paginator.remove_current_page() - await current_suggestion.resolve( - was_approved=False, - state=self.bot.state, - resolved_by=ih.interaction.author.id, + await self.resolve_queued_suggestion( + ih, queued_suggestion=current_suggestion, was_approved=False ) - - guild_id = ih.interaction.guild_id - try: - guild_config: GuildConfig = await GuildConfig.from_id(guild_id, self.state) - icon_url = await Guild.try_fetch_icon_url(guild_id, self.state) - guild = self.state.guild_cache.get_entry(guild_id) - embed: disnake.Embed = disnake.Embed( - description=self.bot.get_localized_string( - "QUEUE_INNER_USER_REJECTED", ih - ), - colour=self.bot.colors.embed_color, - timestamp=self.state.now, - ) - embed.set_author( - name=guild.name, - icon_url=icon_url, - ) - embed.set_footer(text=f"Guild ID {guild_id}") - user = await self.bot.get_or_fetch_user( - current_suggestion.suggestion_author_id - ) - user_config: UserConfig = await UserConfig.from_id( - current_suggestion.suggestion_author_id, self.bot.state - ) - if ( - not user_config.dm_messages_disabled - and not guild_config.dm_messages_disabled - ): - await user.send(embed=embed) - except disnake.HTTPException: - log.debug( - "Failed to DM %s regarding their queued suggestion", - current_suggestion.suggestion_author_id, - ) - + await paginator.remove_current_page() await ih.send(translation_key="PAGINATION_INNER_QUEUE_REJECTED") async def info(self, ih: InteractionHandler): @@ -253,3 +286,51 @@ async def view( ], ) self.paginator_objects.add_entry(pid, paginator) + + async def accept_queued_suggestion( + self, ih: InteractionHandler, message_id: int, channel_id: int, button + ): + current_suggestion: QueuedSuggestion = await QueuedSuggestion.from_message_id( + message_id, channel_id, self.state + ) + if current_suggestion.is_resolved: + return await ih.send(translation_key="QUEUE_INNER_ALREADY_RESOLVED") + + # By here we need to do something about resolving it + suggestion: None = None + guild_id = ih.interaction.guild_id + try: + await paginator.remove_current_page() + suggestion: Suggestion = await current_suggestion.resolve( + was_approved=True, + state=self.bot.state, + resolved_by=ih.interaction.author.id, + ) + guild_config: GuildConfig = await GuildConfig.from_id(guild_id, self.state) + icon_url = await Guild.try_fetch_icon_url(guild_id, self.state) + guild = self.state.guild_cache.get_entry(guild_id) + await suggestion.setup_initial_messages( + guild_config=guild_config, + interaction=ih.interaction, + state=self.state, + bot=self.bot, + cog=self.bot.get_cog("SuggestionsCog"), + guild=guild, + icon_url=icon_url, + comes_from_queue=True, + ) + except: + # Throw it back in the queue on error + current_suggestion.resolved_by = None + current_suggestion.resolved_at = None + current_suggestion.still_in_queue = True + await self.bot.state.queued_suggestions_db.update( + current_suggestion, current_suggestion + ) + + if suggestion is not None: + await self.bot.state.suggestions_db.delete(suggestion) + + raise + + await ih.send(translation_key="PAGINATION_INNER_QUEUE_ACCEPTED") diff --git a/suggestions/exceptions.py b/suggestions/exceptions.py index 0ab460d..7a67708 100644 --- a/suggestions/exceptions.py +++ b/suggestions/exceptions.py @@ -19,6 +19,10 @@ class MissingLogsChannel(CheckFailure): """This command requires a logs channel to run.""" +class MissingQueueLogsChannel(CheckFailure): + """This command requires a queue logs channel to run.""" + + class ErrorHandled(disnake.DiscordException): """This tells error handlers the error was already handled, and can be ignored.""" diff --git a/suggestions/interaction_handler.py b/suggestions/interaction_handler.py index d53ce43..83c7770 100644 --- a/suggestions/interaction_handler.py +++ b/suggestions/interaction_handler.py @@ -21,11 +21,13 @@ class InteractionHandler: def __init__( self, - interaction: disnake.Interaction | disnake.GuildCommandInteraction, + interaction: disnake.Interaction + | disnake.GuildCommandInteraction + | disnake.MessageInteraction, ephemeral: bool, with_message: bool, ): - self.interaction: disnake.Interaction | disnake.GuildCommandInteraction = ( + self.interaction: disnake.Interaction | disnake.GuildCommandInteraction | disnake.MessageInteraction = ( interaction ) self.ephemeral: bool = ephemeral diff --git a/suggestions/locales/en_GB.json b/suggestions/locales/en_GB.json index 401c717..ad9470c 100644 --- a/suggestions/locales/en_GB.json +++ b/suggestions/locales/en_GB.json @@ -107,7 +107,7 @@ "PAGINATION_INNER_QUEUE_REJECTED": "I have removed that suggestion from the queue.", "QUEUE_VIEW_INNER_NOTHING_QUEUED": "Your guild has no suggestions in the queue.", "QUEUE_VIEW_INNER_PRIOR_QUEUE": "These suggestions were queued before your guild disabled the suggestions queue.", - "QUEUE_INNER_USER_REJECTED": "Your queued suggestion was rejected.", + "QUEUE_INNER_USER_REJECTED": "Your queued suggestion was rejected. For your reference, I have included the rejected suggestion below.", "CONFIG_GET_INNER_IMAGES_IN_SUGGESTIONS_SET": "can", "CONFIG_GET_INNER_IMAGES_IN_SUGGESTIONS_NOT_SET": "cannot", "CONFIG_GET_INNER_IMAGES_IN_SUGGESTIONS_MESSAGE": "This guild {} have images in suggestions.", diff --git a/suggestions/locales/en_US.json b/suggestions/locales/en_US.json index 4d3cbc5..fbbe418 100644 --- a/suggestions/locales/en_US.json +++ b/suggestions/locales/en_US.json @@ -105,9 +105,10 @@ "PAGINATION_INNER_QUEUE_CANCELLED": "I have cancelled this queue for you.", "PAGINATION_INNER_QUEUE_ACCEPTED": "I have accepted that suggestion from the queue.", "PAGINATION_INNER_QUEUE_REJECTED": "I have removed that suggestion from the queue.", - "QUEUE_VIEW_INNER_NOTHING_QUEUED": "Your guild has no suggestions in the queue.", + "QUEUE_VIEW_INNER_NOTHING_QUEUED": "Your guild has no suggestions in the virtual queue.", "QUEUE_VIEW_INNER_PRIOR_QUEUE": "These suggestions were queued before your guild disabled the suggestions queue.", - "QUEUE_INNER_USER_REJECTED": "Your queued suggestion was rejected.", + "QUEUE_INNER_USER_REJECTED": "Your queued suggestion was rejected. For your reference, I have included the rejected suggestion below.", + "QUEUE_INNER_ALREADY_RESOLVED": "This queued suggestion has already been resolved.", "CONFIG_GET_INNER_IMAGES_IN_SUGGESTIONS_SET": "can", "CONFIG_GET_INNER_IMAGES_IN_SUGGESTIONS_NOT_SET": "cannot", "CONFIG_GET_INNER_IMAGES_IN_SUGGESTIONS_MESSAGE": "This guild {} have images in suggestions.", diff --git a/suggestions/objects/guild_config.py b/suggestions/objects/guild_config.py index 1f1b803..a3bd75a 100644 --- a/suggestions/objects/guild_config.py +++ b/suggestions/objects/guild_config.py @@ -20,6 +20,7 @@ def __init__( keep_logs: bool = False, dm_messages_disabled: bool = False, log_channel_id: Optional[int] = None, + queued_log_channel_id: Optional[int] = None, threads_for_suggestions: bool = True, suggestions_channel_id: Optional[int] = None, can_have_anonymous_suggestions: bool = False, @@ -33,6 +34,7 @@ def __init__( self._id: int = _id self.keep_logs: bool = keep_logs self.log_channel_id: Optional[int] = log_channel_id + self.queued_log_channel_id: Optional[int] = queued_log_channel_id self.auto_archive_threads: bool = auto_archive_threads self.dm_messages_disabled: bool = dm_messages_disabled self.anonymous_resolutions: bool = anonymous_resolutions @@ -89,6 +91,7 @@ def as_dict(self) -> Dict: "keep_logs": self.keep_logs, "blocked_users": list(self.blocked_users), "log_channel_id": self.log_channel_id, + "queued_log_channel_id": self.queued_log_channel_id, "auto_archive_threads": self.auto_archive_threads, "dm_messages_disabled": self.dm_messages_disabled, "suggestions_channel_id": self.suggestions_channel_id, diff --git a/suggestions/objects/queued_suggestion.py b/suggestions/objects/queued_suggestion.py index 1006138..e36148f 100644 --- a/suggestions/objects/queued_suggestion.py +++ b/suggestions/objects/queued_suggestion.py @@ -4,9 +4,12 @@ import logging from typing import Optional, TYPE_CHECKING, overload +from alaric import AQ +from alaric.comparison import EQ +from alaric.logical import AND from disnake import Embed -from suggestions.exceptions import UnhandledError +from suggestions.exceptions import UnhandledError, SuggestionNotFound from suggestions.objects import Suggestion if TYPE_CHECKING: @@ -31,13 +34,17 @@ def __init__( resolution_note: Optional[str] = None, resolved_at: Optional[datetime.datetime] = None, related_suggestion_id: Optional[str] = None, + message_id: Optional[int] = None, + channel_id: Optional[int] = None, ): self._id: str = _id self.guild_id: int = guild_id self.suggestion: str = suggestion self.is_anonymous: bool = is_anonymous - self.still_in_queue: bool = still_in_queue self.image_url: Optional[str] = image_url + self.still_in_queue: bool = still_in_queue + self.channel_id: Optional[int] = channel_id + self.message_id: Optional[int] = message_id self.resolved_by: Optional[int] = resolved_by self.created_at: datetime.datetime = created_at self.suggestion_author_id: int = suggestion_author_id @@ -49,6 +56,52 @@ def __init__( # this field will be the id of the created suggestion self.related_suggestion_id: Optional[str] = related_suggestion_id + @property + def is_resolved(self) -> bool: + return self.resolved_by is not None + + @classmethod + async def from_message_id( + cls, message_id: int, channel_id: int, state: State + ) -> QueuedSuggestion: + """Return a suggestion from its sent message. + + Useful for message commands. + + Parameters + ---------- + message_id : int + The message id + channel_id : int + The channel id + state : State + Our internal state + + Returns + ------- + QueuedSuggestion + The found suggestion + + Raises + ------ + SuggestionNotFound + No suggestion exists for this data + """ + suggestion: QueuedSuggestion | None = await state.queued_suggestions_db.find( + AQ( + AND( + EQ("message_id", message_id), + EQ("channel_id", channel_id), + ) + ) + ) + if not suggestion: + raise SuggestionNotFound( + f"This message does not look like a suggestions message." + ) + + return suggestion + @classmethod async def new( cls, @@ -127,6 +180,10 @@ def as_dict(self) -> dict: if self.related_suggestion_id: data["related_suggestion_id"] = self.related_suggestion_id + if self.message_id is not None: + data["message_id"] = self.message_id + data["channel_id"] = self.channel_id + return data async def as_embed(self, bot: SuggestionsBot) -> Embed: diff --git a/suggestions/state.py b/suggestions/state.py index 02c2dc1..cd24e41 100644 --- a/suggestions/state.py +++ b/suggestions/state.py @@ -6,7 +6,7 @@ import random import string from datetime import timedelta -from typing import TYPE_CHECKING, List, Dict, Set +from typing import TYPE_CHECKING, List, Dict, Set, Any import disnake from alaric import AQ @@ -15,6 +15,8 @@ from alaric.meta import Negate from alaric.projections import PROJECTION, SHOW from commons.caching import NonExistentEntry, TimedCache +from disnake import Thread +from disnake.abc import GuildChannel, PrivateChannel from suggestions.objects import GuildConfig, UserConfig @@ -51,6 +53,9 @@ def __init__(self, database: SuggestionsMongoManager, bot: SuggestionsBot): lazy_eviction=False, ttl_from_last_access=True, ) + self.object_cache: TimedCache[int, Any] = TimedCache( + global_ttl=timedelta(hours=1), lazy_eviction=False + ) self.guild_configs: TimedCache = TimedCache( global_ttl=timedelta(minutes=30), @@ -249,6 +254,31 @@ async def load(self): for entry in error_ids: self.existing_error_ids.add(entry["_id"]) + async def fetch_channel(self, channel_id: int) -> disnake.TextChannel: + try: + return self.object_cache.get_entry(channel_id) + except NonExistentEntry: + chan = await self.bot.fetch_channel(channel_id) + self.object_cache.add_entry(channel_id, chan) + return chan # type: ignore + + async def fetch_user(self, user_id: int) -> disnake.User: + try: + return self.object_cache.get_entry(user_id) + except NonExistentEntry: + user = await self.bot.fetch_user(user_id) + self.object_cache.add_entry(user_id, user) + return user + + async def fetch_guild(self, guild_id: int) -> disnake.Guild: + # Need guild cache instead of object as used else where + try: + return self.guild_cache.get_entry(guild_id) + except NonExistentEntry: + guild = await self.bot.fetch_guild(guild_id) + self.guild_cache.add_entry(guild_id, guild) + return guild + async def evict_caches(self): """Cleans the caches every 10 minutes""" while not self.is_closing: From a0c5d8a6c452a9c674abf3409a0906a97c89dcbe Mon Sep 17 00:00:00 2001 From: skelmis Date: Sun, 25 Feb 2024 17:26:09 +1300 Subject: [PATCH 04/12] fix: virtual queue approval and rejection system --- suggestions/bot.py | 16 +++++++++-- suggestions/core/suggestions_queue.py | 40 ++++++++++++++++++++------- suggestions/interaction_handler.py | 8 ++++-- 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/suggestions/bot.py b/suggestions/bot.py index 229b8bb..23f4c00 100644 --- a/suggestions/bot.py +++ b/suggestions/bot.py @@ -241,11 +241,16 @@ async def process_commands(self, message: disnake.Message): await self.invoke(ctx) async def _push_slash_error_stats( - self, interaction: disnake.ApplicationCommandInteraction + self, + interaction: disnake.ApplicationCommandInteraction | disnake.MessageInteraction, ): - stat_type: Optional[StatsEnum] = StatsEnum.from_command_name( + name = ( interaction.application_command.qualified_name + if isinstance(interaction, disnake.ApplicationCommandInteraction) + else interaction.data["custom_id"].split(":")[0] # Button name ) + + stat_type: Optional[StatsEnum] = StatsEnum.from_command_name(name) if not stat_type: return @@ -545,7 +550,12 @@ async def on_slash_command_error( ) return - if interaction.deferred_without_send: + ih: InteractionHandler = await InteractionHandler.fetch_handler( + interaction.id, self + ) + if interaction.deferred_without_send or ( + ih is not None and not ih.has_sent_something + ): gid = interaction.guild_id if interaction.guild_id else None # Fix "Bot is thinking" hanging on edge cases... await interaction.send( diff --git a/suggestions/core/suggestions_queue.py b/suggestions/core/suggestions_queue.py index f298f92..2d14e90 100644 --- a/suggestions/core/suggestions_queue.py +++ b/suggestions/core/suggestions_queue.py @@ -1,9 +1,9 @@ from __future__ import annotations +import functools import logging from datetime import timedelta -from typing import TYPE_CHECKING - +from typing import TYPE_CHECKING, Callable import disnake from alaric import AQ @@ -26,6 +26,20 @@ log = logging.getLogger(__name__) +def wrap_with_error_handler(): + def decorator(func: Callable): + @functools.wraps(func) + async def wrapper(*args, **kwargs): + try: + return await func(*args, **kwargs) + except Exception as e: + await args[0].bot.on_slash_command_error(args[1].interaction, e) + + return wrapper + + return decorator + + class SuggestionsQueue: """ Approach to suggestions queue. @@ -100,10 +114,6 @@ async def resolve_queued_suggestion( guild_id = ih.interaction.guild_id suggestion: Suggestion | None = None try: - queued_suggestion.resolved_by = ih.interaction.author.id - queued_suggestion.resolved_at = self.bot.state.now - queued_suggestion.still_in_queue = False - guild_config: GuildConfig = await GuildConfig.from_id(guild_id, self.state) # Send the message to the relevant channel if required if was_approved: @@ -123,9 +133,8 @@ async def resolve_queued_suggestion( icon_url=icon_url, comes_from_queue=True, ) - # We dont send the user a message here because setup_initial_messages - # does this for us - + # We dont send the user a message here because + # setup_initial_messages does this for us else: # We may need to send this rejected suggestion to a logs channel if guild_config.queued_log_channel_id: @@ -169,19 +178,29 @@ async def resolve_queued_suggestion( await user.send( embeds=[embed, await queued_suggestion.as_embed(self.bot)] ) - except: # Don't remove from queue on failure if suggestion is not None: await self.bot.state.suggestions_db.delete(suggestion) + if suggestion.message_id is not None: + channel: disnake.TextChannel = await self.state.fetch_channel( + suggestion.channel_id + ) + message = await channel.fetch_message(suggestion.message_id) + await message.delete() + # Re-raise for the bot handler raise else: + queued_suggestion.resolved_by = ih.interaction.author.id + queued_suggestion.resolved_at = self.bot.state.now + queued_suggestion.still_in_queue = False await self.bot.state.queued_suggestions_db.update( queued_suggestion, queued_suggestion ) + @wrap_with_error_handler() async def approve_button(self, ih: InteractionHandler, pid: str): paginator = await self.get_paginator_for(pid, ih) current_suggestion: QueuedSuggestion = ( @@ -193,6 +212,7 @@ async def approve_button(self, ih: InteractionHandler, pid: str): await paginator.remove_current_page() await ih.send(translation_key="PAGINATION_INNER_QUEUE_ACCEPTED") + @wrap_with_error_handler() async def reject_button(self, ih: InteractionHandler, pid: str): paginator = await self.get_paginator_for(pid, ih) current_suggestion = await paginator.get_current_queued_suggestion() diff --git a/suggestions/interaction_handler.py b/suggestions/interaction_handler.py index 83c7770..caabd86 100644 --- a/suggestions/interaction_handler.py +++ b/suggestions/interaction_handler.py @@ -3,6 +3,7 @@ from typing import cast, TYPE_CHECKING import disnake +from commons.caching import NonExistentEntry from suggestions.exceptions import ConflictingHandlerInformation @@ -100,6 +101,9 @@ async def new_handler( @classmethod async def fetch_handler( cls, application_id: int, bot: SuggestionsBot - ) -> InteractionHandler: + ) -> InteractionHandler | None: """Fetch a registered handler for the given interaction.""" - return bot.state.interaction_handlers.get_entry(application_id) + try: + return bot.state.interaction_handlers.get_entry(application_id) + except NonExistentEntry: + return None From 6bc1b860b59fd4cf593b55a720948bf59e50e67c Mon Sep 17 00:00:00 2001 From: skelmis Date: Sun, 25 Feb 2024 19:02:58 +1300 Subject: [PATCH 05/12] feat: add initial physical queue --- suggestions/bot.py | 12 +++ suggestions/codes.py | 1 + suggestions/cogs/guild_config_cog.py | 114 +++++++++++++++++++++++ suggestions/cogs/suggestion_cog.py | 65 ++++++++++++- suggestions/cogs/suggestion_queue_cog.py | 20 ++-- suggestions/core/suggestions_queue.py | 22 ++++- suggestions/exceptions.py | 4 + suggestions/locales/en_US.json | 6 +- suggestions/objects/guild_config.py | 6 ++ suggestions/objects/queued_suggestion.py | 8 +- suggestions/stats.py | 4 + 11 files changed, 248 insertions(+), 14 deletions(-) diff --git a/suggestions/bot.py b/suggestions/bot.py index 23f4c00..825e67d 100644 --- a/suggestions/bot.py +++ b/suggestions/bot.py @@ -37,6 +37,7 @@ BlocklistedUser, PartialResponse, MissingQueueLogsChannel, + MissingPermissionsToAccessQueueChannel, ) from suggestions.http_error_parser import try_parse_http_error from suggestions.interaction_handler import InteractionHandler @@ -399,6 +400,17 @@ async def on_slash_command_error( ephemeral=True, ) + elif isinstance(exception, MissingPermissionsToAccessQueueChannel): + return await interaction.send( + embed=self.error_embed( + title="Missing permissions within queue logs channel", + description="The bot does not have the required permissions in your queue channel. " + "Please contact an administrator and ask them to fix this.", + error=error, + error_code=ErrorCode.MISSING_PERMISSIONS_IN_QUEUE_CHANNEL, + ) + ) + elif isinstance(exception, commands.MissingPermissions): perms = ",".join(i for i in exception.missing_permissions) return await interaction.send( diff --git a/suggestions/codes.py b/suggestions/codes.py index c9fb800..c962861 100644 --- a/suggestions/codes.py +++ b/suggestions/codes.py @@ -27,6 +27,7 @@ class ErrorCode(IntEnum): MISSING_QUEUE_CHANNEL = 21 BLOCKLISTED_USER = 22 MISSING_QUEUE_LOG_CHANNEL = 23 + MISSING_PERMISSIONS_IN_QUEUE_CHANNEL = 24 @classmethod def from_value(cls, value: int) -> ErrorCode: diff --git a/suggestions/cogs/guild_config_cog.py b/suggestions/cogs/guild_config_cog.py index 58528a2..b792233 100644 --- a/suggestions/cogs/guild_config_cog.py +++ b/suggestions/cogs/guild_config_cog.py @@ -11,6 +11,7 @@ from suggestions import Stats from suggestions.cooldown_bucket import InteractionBucket from suggestions.exceptions import InvalidGuildConfigOption +from suggestions.interaction_handler import InteractionHandler from suggestions.objects import GuildConfig from suggestions.stats import StatsEnum @@ -97,6 +98,83 @@ async def logs( self.stats.type.GUILD_CONFIG_LOG_CHANNEL, ) + @config.sub_command() + async def queue_channel( + self, + interaction: disnake.GuildCommandInteraction, + channel: disnake.TextChannel, + ): + """Set your guilds physical suggestions queue channel.""" + ih: InteractionHandler = await InteractionHandler.new_handler(interaction) + guild_config: GuildConfig = await GuildConfig.from_id( + interaction.guild_id, self.state + ) + try: + message = await channel.send("This is a test message and can be ignored.") + await message.delete() + except disnake.Forbidden: + return await ih.send( + f"I do not have permissions to delete messages in {channel.mention}. " + f"Please give me permissions and run this command again.", + ) + + guild_config.queued_channel_id = channel.id + self.state.refresh_guild_config(guild_config) + await self.state.guild_config_db.upsert(guild_config, guild_config) + await ih.send( + self.bot.get_localized_string( + "CONFIG_QUEUE_CHANNEL_INNER_MESSAGE", + ih, + extras={"CHANNEL": channel.mention}, + ) + ) + log.debug( + "User %s changed physical queue channel to %s in guild %s", + interaction.author.id, + channel.id, + interaction.guild_id, + ) + await self.stats.log_stats( + interaction.author.id, + interaction.guild_id, + self.stats.type.GUILD_CONFIG_QUEUE_CHANNEL, + ) + + @config.sub_command() + async def queue_log_channel( + self, + interaction: disnake.GuildCommandInteraction, + channel: disnake.TextChannel = None, + ): + """Set your guilds suggestion queue log channel for rejected suggestions.""" + ih: InteractionHandler = await InteractionHandler.new_handler(interaction) + guild_config: GuildConfig = await GuildConfig.from_id( + interaction.guild_id, self.state + ) + guild_config.queued_log_channel_id = channel.id if channel else None + self.state.refresh_guild_config(guild_config) + await self.state.guild_config_db.upsert(guild_config, guild_config) + key = ( + "CONFIG_QUEUE_CHANNEL_INNER_MESSAGE_REMOVED" + if channel is None + else "CONFIG_QUEUE_CHANNEL_INNER_MESSAGE" + ) + msg = self.bot.get_locale(key, interaction.locale) + if channel is not None: + msg = msg.format(channel.mention) + await ih.send(msg) + log.debug( + "User %s changed rejected queue log channel to %s in guild %s", + interaction.author.id, + channel.id if channel is not None else None, + interaction.guild_id, + ) + await self.stats.log_stats( + interaction.author.id, + interaction.guild_id, + self.stats.type.GUILD_CONFIG_REJECTED_QUEUE_CHANNEL, + ) + @config.sub_command() async def get( self, @@ -642,6 +720,42 @@ async def anonymous_resolutions_disable( self.stats.type.GUILD_ANONYMOUS_RESOLUTIONS_DISABLE, ) + @config.sub_command_group() + async def use_physical_queue(self, interaction: disnake.GuildCommandInteraction): + pass + + @use_physical_queue.sub_command(name="enable") + async def use_physical_queue_enable( + self, interaction: disnake.GuildCommandInteraction + ): + """Set the queue to use a channel for queuing suggestions.""" + await self.modify_guild_config( + interaction, + "virtual_suggestion_queue", + False, + self.bot.get_localized_string( + "CONFIG_USE_PHYSICAL_QUEUE_ENABLE_INNER_MESSAGE", interaction + ), + "Enabled physical queue on suggestions for guild %s", + self.stats.type.GUILD_PHYSICAL_QUEUE_ENABLE, + ) + + @use_physical_queue.sub_command(name="disable") + async def use_physical_queue_disable( + self, interaction: disnake.GuildCommandInteraction + ): + """Use a virtual queue for suggestions in this guild.""" + await self.modify_guild_config( + interaction, + "virtual_suggestion_queue", + True, + self.bot.get_localized_string( + "CONFIG_USE_PHYSICAL_QUEUE_DISABLE_INNER_MESSAGE", interaction + ), + "Disabled physical queue on suggestions for guild %s", + self.stats.type.GUILD_PHYSICAL_QUEUE_DISABLE, + ) + @config.sub_command_group() async def images_in_suggestions(self, interaction: disnake.GuildCommandInteraction): pass diff --git a/suggestions/cogs/suggestion_cog.py b/suggestions/cogs/suggestion_cog.py index e12ba75..64d3545 100644 --- a/suggestions/cogs/suggestion_cog.py +++ b/suggestions/cogs/suggestion_cog.py @@ -13,7 +13,14 @@ from suggestions import checks, Stats, ErrorCode from suggestions.clunk2 import update_suggestion_message from suggestions.cooldown_bucket import InteractionBucket -from suggestions.exceptions import SuggestionTooLong, ErrorHandled +from suggestions.core import SuggestionsQueue +from suggestions.exceptions import ( + SuggestionTooLong, + ErrorHandled, + MissingPermissionsToAccessQueueChannel, + MissingQueueLogsChannel, +) +from suggestions.interaction_handler import InteractionHandler from suggestions.objects import Suggestion, GuildConfig, UserConfig, QueuedSuggestion from suggestions.objects.suggestion import SuggestionState @@ -31,6 +38,8 @@ def __init__(self, bot: SuggestionsBot): self.stats: Stats = self.bot.stats self.suggestions_db: Document = self.bot.db.suggestions + self.qs_core: SuggestionsQueue = SuggestionsQueue(bot) + @components.button_listener() async def suggestion_up_vote( self, inter: disnake.MessageInteraction, *, suggestion_id: str @@ -149,6 +158,28 @@ async def suggestion_down_vote( await update_suggestion_message(suggestion=suggestion, bot=self.bot) # log.debug("Member %s down voted suggestion %s", member_id, suggestion_id) + @components.button_listener() + async def queue_approve(self, inter: disnake.MessageInteraction): + ih = await InteractionHandler.new_handler(inter) + qs = await QueuedSuggestion.from_message_id( + inter.message.id, inter.message.channel.id, self.state + ) + await self.qs_core.resolve_queued_suggestion( + ih, queued_suggestion=qs, was_approved=True + ) + await ih.send(translation_key="PAGINATION_INNER_QUEUE_ACCEPTED") + + @components.button_listener() + async def queue_reject(self, inter: disnake.MessageInteraction): + ih = await InteractionHandler.new_handler(inter) + qs = await QueuedSuggestion.from_message_id( + inter.message.id, inter.message.channel.id, self.state + ) + await self.qs_core.resolve_queued_suggestion( + ih, queued_suggestion=qs, was_approved=False + ) + await ih.send(translation_key="PAGINATION_INNER_QUEUE_REJECTED") + @commands.slash_command( dm_permission=False, ) @@ -199,7 +230,7 @@ async def suggest( raise ErrorHandled if guild_config.uses_suggestion_queue: - await QueuedSuggestion.new( + qs = await QueuedSuggestion.new( suggestion=suggestion, guild_id=interaction.guild_id, state=self.state, @@ -207,6 +238,36 @@ async def suggest( image_url=image_url, is_anonymous=anonymously, ) + if not guild_config.virtual_suggestion_queue: + # Need to send to a channel + if guild_config.queued_channel_id is None: + raise MissingQueueLogsChannel + + try: + queue_channel = await self.bot.state.fetch_channel( + guild_config.queued_channel_id + ) + except disnake.Forbidden as e: + raise MissingPermissionsToAccessQueueChannel from e + + qs_embed: disnake.Embed = await qs.as_embed(self.bot) + msg = await queue_channel.send( + embed=qs_embed, + components=[ + disnake.ui.Button( + label="Approve queued suggestion", + custom_id=await self.queue_approve.build_custom_id(), + ), + disnake.ui.Button( + label="Reject queued suggestion", + custom_id=await self.queue_reject.build_custom_id(), + ), + ], + ) + qs.message_id = msg.id + qs.channel_id = msg.channel.id + await self.bot.db.queued_suggestions.upsert(qs, qs) + log.debug( "User %s created new queued suggestion in guild %s", interaction.author.id, diff --git a/suggestions/cogs/suggestion_queue_cog.py b/suggestions/cogs/suggestion_queue_cog.py index 9d35d07..66c7a8f 100644 --- a/suggestions/cogs/suggestion_queue_cog.py +++ b/suggestions/cogs/suggestion_queue_cog.py @@ -50,12 +50,20 @@ async def stop_button(self, inter: disnake.MessageInteraction, *, pid: str): await self.core.stop_button(await InteractionHandler.new_handler(inter), pid) @components.button_listener() - async def approve_button(self, inter: disnake.MessageInteraction, *, pid: str): - await self.core.approve_button(await InteractionHandler.new_handler(inter), pid) + async def virtual_approve_button( + self, inter: disnake.MessageInteraction, *, pid: str + ): + await self.core.virtual_approve_button( + await InteractionHandler.new_handler(inter), pid + ) @components.button_listener() - async def reject_button(self, inter: disnake.MessageInteraction, *, pid: str): - await self.core.reject_button(await InteractionHandler.new_handler(inter), pid) + async def virtual_reject_button( + self, inter: disnake.MessageInteraction, *, pid: str + ): + await self.core.virtual_reject_button( + await InteractionHandler.new_handler(inter), pid + ) @components.button_listener() async def accept_queued_suggestion(self, inter: disnake.MessageInteraction): @@ -91,8 +99,8 @@ async def view(self, interaction: disnake.GuildCommandInteraction): self.previous_button, self.next_button, self.stop_button, - self.approve_button, - self.reject_button, + self.virtual_approve_button, + self.virtual_reject_button, ) diff --git a/suggestions/core/suggestions_queue.py b/suggestions/core/suggestions_queue.py index 2d14e90..fda3089 100644 --- a/suggestions/core/suggestions_queue.py +++ b/suggestions/core/suggestions_queue.py @@ -7,8 +7,9 @@ import disnake from alaric import AQ -from alaric.comparison import EQ +from alaric.comparison import EQ, Exists from alaric.logical import AND +from alaric.meta import Negate from alaric.projections import Projection, SHOW from commons.caching import NonExistentEntry, TimedCache from disnake import Guild @@ -115,6 +116,13 @@ async def resolve_queued_suggestion( suggestion: Suggestion | None = None try: guild_config: GuildConfig = await GuildConfig.from_id(guild_id, self.state) + + # If sent to physical queue, delete it + if not queued_suggestion.is_in_virtual_queue: + chan = await self.state.fetch_channel(queued_suggestion.channel_id) + msg = await chan.fetch_message(queued_suggestion.message_id) + await msg.delete() + # Send the message to the relevant channel if required if was_approved: # Send this message through to the guilds suggestion channel @@ -201,7 +209,7 @@ async def resolve_queued_suggestion( ) @wrap_with_error_handler() - async def approve_button(self, ih: InteractionHandler, pid: str): + async def virtual_approve_button(self, ih: InteractionHandler, pid: str): paginator = await self.get_paginator_for(pid, ih) current_suggestion: QueuedSuggestion = ( await paginator.get_current_queued_suggestion() @@ -213,7 +221,7 @@ async def approve_button(self, ih: InteractionHandler, pid: str): await ih.send(translation_key="PAGINATION_INNER_QUEUE_ACCEPTED") @wrap_with_error_handler() - async def reject_button(self, ih: InteractionHandler, pid: str): + async def virtual_reject_button(self, ih: InteractionHandler, pid: str): paginator = await self.get_paginator_for(pid, ih) current_suggestion = await paginator.get_current_queued_suggestion() await self.resolve_queued_suggestion( @@ -257,7 +265,13 @@ async def view( guild_id = ih.interaction.guild_id guild_config: GuildConfig = await GuildConfig.from_id(guild_id, self.state) data: list = await self.queued_suggestions_db.find_many( - AQ(AND(EQ("guild_id", guild_id), EQ("still_in_queue", True))), + AQ( + AND( + EQ("guild_id", guild_id), + EQ("still_in_queue", True), + Negate(Exists("message_id")), + ) + ), projections=Projection(SHOW("_id")), try_convert=False, ) diff --git a/suggestions/exceptions.py b/suggestions/exceptions.py index 7a67708..2749866 100644 --- a/suggestions/exceptions.py +++ b/suggestions/exceptions.py @@ -23,6 +23,10 @@ class MissingQueueLogsChannel(CheckFailure): """This command requires a queue logs channel to run.""" +class MissingPermissionsToAccessQueueChannel(disnake.DiscordException): + """The bot does not have permissions to interact with the queue channel.""" + + class ErrorHandled(disnake.DiscordException): """This tells error handlers the error was already handled, and can be ignored.""" diff --git a/suggestions/locales/en_US.json b/suggestions/locales/en_US.json index fbbe418..90ff5cb 100644 --- a/suggestions/locales/en_US.json +++ b/suggestions/locales/en_US.json @@ -62,6 +62,8 @@ "SUGGESTION_OBJECT_LOCK_THREAD": "Locking this thread as the suggestion has reached a resolution.", "CONFIG_CHANNEL_INNER_MESSAGE": "I have set this guilds suggestion channel to {}", "CONFIG_LOGS_INNER_MESSAGE": "I have set this guilds log channel to {}", + "CONFIG_QUEUE_CHANNEL_INNER_MESSAGE": "I have set this guilds suggestion queue channel to $CHANNEL", + "CONFIG_QUEUE_CHANNEL_INNER_MESSAGE_REMOVED": "I have disabled sending rejected queue suggestions to a channel.", "CONFIG_GET_INNER_BASE_EMBED_DESCRIPTION": "Configuration for {}\n", "CONFIG_GET_INNER_PARTIAL_LOG_CHANNEL_SET": "Log channel: <#{}>", "CONFIG_GET_INNER_PARTIAL_LOG_CHANNEL_NOT_SET": "Not set", @@ -122,5 +124,7 @@ "SUGGESTION_ID_NAME": "suggestion_id", "SUGGESTION_ID_DESCRIPTION": "The suggestions ID you wish to reference.", "USER_ID_NAME": "user_id", - "USER_ID_DESCRIPTION": "The users discord id." + "USER_ID_DESCRIPTION": "The users discord id.", + "CONFIG_USE_PHYSICAL_QUEUE_ENABLE_INNER_MESSAGE": "If the suggestions queue is enabled, all new suggestions will go to a physical queue.", + "CONFIG_USE_PHYSICAL_QUEUE_DISABLE_INNER_MESSAGE": "If the suggestions queue is enabled, all new suggestions will go to a virtual queue." } diff --git a/suggestions/objects/guild_config.py b/suggestions/objects/guild_config.py index a3bd75a..027596e 100644 --- a/suggestions/objects/guild_config.py +++ b/suggestions/objects/guild_config.py @@ -20,12 +20,14 @@ def __init__( keep_logs: bool = False, dm_messages_disabled: bool = False, log_channel_id: Optional[int] = None, + queued_channel_id: Optional[int] = None, queued_log_channel_id: Optional[int] = None, threads_for_suggestions: bool = True, suggestions_channel_id: Optional[int] = None, can_have_anonymous_suggestions: bool = False, auto_archive_threads: bool = False, uses_suggestion_queue: bool = False, + virtual_suggestion_queue: bool = True, can_have_images_in_suggestions: bool = True, anonymous_resolutions: bool = False, blocked_users: Optional[list[int]] = None, @@ -34,11 +36,13 @@ def __init__( self._id: int = _id self.keep_logs: bool = keep_logs self.log_channel_id: Optional[int] = log_channel_id + self.queued_channel_id: Optional[int] = queued_channel_id self.queued_log_channel_id: Optional[int] = queued_log_channel_id self.auto_archive_threads: bool = auto_archive_threads self.dm_messages_disabled: bool = dm_messages_disabled self.anonymous_resolutions: bool = anonymous_resolutions self.uses_suggestion_queue: bool = uses_suggestion_queue + self.virtual_suggestion_queue: bool = virtual_suggestion_queue self.threads_for_suggestions: bool = threads_for_suggestions self.suggestions_channel_id: Optional[int] = suggestions_channel_id self.can_have_anonymous_suggestions: bool = can_have_anonymous_suggestions @@ -91,12 +95,14 @@ def as_dict(self) -> Dict: "keep_logs": self.keep_logs, "blocked_users": list(self.blocked_users), "log_channel_id": self.log_channel_id, + "queued_channel_id": self.queued_channel_id, "queued_log_channel_id": self.queued_log_channel_id, "auto_archive_threads": self.auto_archive_threads, "dm_messages_disabled": self.dm_messages_disabled, "suggestions_channel_id": self.suggestions_channel_id, "uses_suggestion_queue": self.uses_suggestion_queue, "anonymous_resolutions": self.anonymous_resolutions, + "virtual_suggestion_queue": self.virtual_suggestion_queue, "threads_for_suggestions": self.threads_for_suggestions, "can_have_anonymous_suggestions": self.can_have_anonymous_suggestions, "can_have_images_in_suggestions": self.can_have_images_in_suggestions, diff --git a/suggestions/objects/queued_suggestion.py b/suggestions/objects/queued_suggestion.py index e36148f..cc861e3 100644 --- a/suggestions/objects/queued_suggestion.py +++ b/suggestions/objects/queued_suggestion.py @@ -60,6 +60,10 @@ def __init__( def is_resolved(self) -> bool: return self.resolved_by is not None + @property + def is_in_virtual_queue(self) -> bool: + return self.message_id is None + @classmethod async def from_message_id( cls, message_id: int, channel_id: int, state: State @@ -144,7 +148,9 @@ async def new( is_anonymous=is_anonymous, ) await state.queued_suggestions_db.insert(suggestion) - return suggestion + + # Try to populate id on returned object + return await state.queued_suggestions_db.find(suggestion.as_dict()) def as_filter(self) -> dict: if not self._id: diff --git a/suggestions/stats.py b/suggestions/stats.py index 28ab7be..b3c5d75 100644 --- a/suggestions/stats.py +++ b/suggestions/stats.py @@ -31,6 +31,8 @@ class StatsEnum(Enum): MEMBER_DM_ENABLE = "member_dm_enable" MEMBER_DM_DISABLE = "member_dm_disable" GUILD_CONFIG_LOG_CHANNEL = "guild_config_log_channel" + GUILD_CONFIG_QUEUE_CHANNEL = "guild_config_queue_channel" + GUILD_CONFIG_REJECTED_QUEUE_CHANNEL = "guild_config_rejected_queue_channel" GUILD_CONFIG_SUGGEST_CHANNEL = "guild_config_suggest_channel" GUILD_CONFIG_GET = "guild_config_get" GUILD_DM_ENABLE = "guild_dm_enable" @@ -45,6 +47,8 @@ class StatsEnum(Enum): GUILD_ANONYMOUS_RESOLUTIONS_DISABLE = "guild_anonymous_resolutions_disable" GUILD_IMAGES_IN_SUGGESTIONS_ENABLE = "guild_images_in_suggestions_enable" GUILD_IMAGES_IN_SUGGESTIONS_DISABLE = "guild_images_in_suggestions_disable" + GUILD_PHYSICAL_QUEUE_ENABLE = "guild_physical_queue_enable" + GUILD_PHYSICAL_QUEUE_DISABLE = "guild_physical_queue_disable" GUILD_KEEPLOGS_ENABLE = "guild_keeplogs_enable" GUILD_KEEPLOGS_DISABLE = "guild_keeplogs_disable" GUILD_ANONYMOUS_ENABLE = "guild_anonymous_enable" From d369371c84e0091bff7184b99ef87619025eafa0 Mon Sep 17 00:00:00 2001 From: skelmis Date: Sun, 25 Feb 2024 19:08:31 +1300 Subject: [PATCH 06/12] fix: messages being sent --- suggestions/cogs/guild_config_cog.py | 2 +- suggestions/locales/en_US.json | 1 + suggestions/objects/queued_suggestion.py | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/suggestions/cogs/guild_config_cog.py b/suggestions/cogs/guild_config_cog.py index b792233..bc2373a 100644 --- a/suggestions/cogs/guild_config_cog.py +++ b/suggestions/cogs/guild_config_cog.py @@ -157,7 +157,7 @@ async def queue_log_channel( key = ( "CONFIG_QUEUE_CHANNEL_INNER_MESSAGE_REMOVED" if channel is None - else "CONFIG_QUEUE_CHANNEL_INNER_MESSAGE" + else "CONFIG_QUEUE_LOG_CHANNEL_INNER_MESSAGE" ) msg = self.bot.get_locale(key, interaction.locale) if channel is not None: diff --git a/suggestions/locales/en_US.json b/suggestions/locales/en_US.json index 90ff5cb..9a92945 100644 --- a/suggestions/locales/en_US.json +++ b/suggestions/locales/en_US.json @@ -63,6 +63,7 @@ "CONFIG_CHANNEL_INNER_MESSAGE": "I have set this guilds suggestion channel to {}", "CONFIG_LOGS_INNER_MESSAGE": "I have set this guilds log channel to {}", "CONFIG_QUEUE_CHANNEL_INNER_MESSAGE": "I have set this guilds suggestion queue channel to $CHANNEL", + "CONFIG_QUEUE_LOG_CHANNEL_INNER_MESSAGE": "I have set this guilds suggestion queue channel to {}", "CONFIG_QUEUE_CHANNEL_INNER_MESSAGE_REMOVED": "I have disabled sending rejected queue suggestions to a channel.", "CONFIG_GET_INNER_BASE_EMBED_DESCRIPTION": "Configuration for {}\n", "CONFIG_GET_INNER_PARTIAL_LOG_CHANNEL_SET": "Log channel: <#{}>", diff --git a/suggestions/objects/queued_suggestion.py b/suggestions/objects/queued_suggestion.py index cc861e3..67212a6 100644 --- a/suggestions/objects/queued_suggestion.py +++ b/suggestions/objects/queued_suggestion.py @@ -207,7 +207,9 @@ async def as_embed(self, bot: SuggestionsBot) -> Embed: ) if not self.is_anonymous: embed.set_thumbnail(user.display_avatar) - embed.set_footer(text=f"Submitter ID: {self.suggestion_author_id}") + embed.set_footer( + text=f"Queued suggestion | Submitter ID: {self.suggestion_author_id}" + ) if self.image_url: embed.set_image(self.image_url) From 7f44e68febae76c72d483e67e6aacfa246f7f848 Mon Sep 17 00:00:00 2001 From: skelmis Date: Sun, 25 Feb 2024 19:12:36 +1300 Subject: [PATCH 07/12] fix: error message for missing queue channel --- suggestions/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suggestions/bot.py b/suggestions/bot.py index 825e67d..6ad6d23 100644 --- a/suggestions/bot.py +++ b/suggestions/bot.py @@ -393,7 +393,7 @@ async def on_slash_command_error( "Missing Queue Logs Channel", "This command requires a queue log channel to use.\n" "Please contact an administrator and ask them to set one up " - "using the following command.\n`/config queue_logs`", + "using the following command.\n`/config queue_channel`", error_code=ErrorCode.MISSING_QUEUE_LOG_CHANNEL, error=error, ), From 3d78a1bf3179440a4800055e6aa5172be13d94b8 Mon Sep 17 00:00:00 2001 From: skelmis Date: Sun, 25 Feb 2024 19:22:40 +1300 Subject: [PATCH 08/12] feat: add items to /config get --- suggestions/cogs/guild_config_cog.py | 60 +++++++++++++++++++++++++++- suggestions/locales/en_US.json | 6 +++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/suggestions/cogs/guild_config_cog.py b/suggestions/cogs/guild_config_cog.py index bc2373a..c55d0a2 100644 --- a/suggestions/cogs/guild_config_cog.py +++ b/suggestions/cogs/guild_config_cog.py @@ -192,6 +192,9 @@ async def get( "Suggestions queue", "Images in suggestions", "Anonymous resolutions", + "Using physical queue", + "Queue channel", + "Queue rejection channel", ], default=None, ), @@ -225,6 +228,33 @@ async def get( ) embed.description += log_channel + elif config == "Queue channel": + log_channel = ( + self.bot.get_locale( + "CONFIG_GET_INNER_PARTIAL_QUEUE_LOG_CHANNEL_SET", interaction.locale + ).format(guild_config.queued_channel_id) + if guild_config.queued_channel_id + else self.bot.get_locale( + "CONFIG_GET_INNER_PARTIAL_QUEUE_LOG_CHANNEL_NOT_SET", + interaction.locale, + ) + ) + embed.description += log_channel + + elif config == "Queue rejection channel": + log_channel = ( + self.bot.get_locale( + "CONFIG_GET_INNER_PARTIAL_QUEUE_REJECTION_LOG_CHANNEL_SET", + interaction.locale, + ).format(guild_config.queued_log_channel_id) + if guild_config.queued_log_channel_id + else self.bot.get_locale( + "CONFIG_GET_INNER_PARTIAL_QUEUE_REJECTION_LOG_CHANNEL_NOT_SET", + interaction.locale, + ) + ) + embed.description += log_channel + elif config == "Suggestions channel": suggestions_channel = ( self.bot.get_locale( @@ -378,6 +408,22 @@ async def send_full_config(self, interaction: disnake.GuildCommandInteraction): interaction.locale, ) ) + queue_channel = ( + f"<#{guild_config.queued_channel_id}>" + if guild_config.queued_channel_id + else self.bot.get_locale( + "CONFIG_GET_INNER_PARTIAL_QUEUE_LOG_CHANNEL_NOT_SET", + interaction.locale, + ) + ) + queue_rejection_channel = ( + f"<#{guild_config.queued_log_channel_id}>" + if guild_config.queued_log_channel_id + else self.bot.get_locale( + "CONFIG_GET_INNER_PARTIAL_QUEUE_REJECTION_LOG_CHANNEL_NOT_SET", + interaction.locale, + ) + ) dm_responses = ( self.bot.get_locale( "CONFIG_GET_INNER_PARTIAL_DM_RESPONSES_NOT_SET", interaction.locale @@ -423,6 +469,16 @@ async def send_full_config(self, interaction: disnake.GuildCommandInteraction): "CONFIG_GET_INNER_ANONYMOUS_SUGGESTIONS_MESSAGE", interaction.locale ).format(anon_text) + physical_queue = ( + self.bot.get_locale( + "CONFIG_GET_INNER_USES_PHYSICAL_QUEUE_SET", interaction.locale + ) + if guild_config.virtual_suggestion_queue + else self.bot.get_locale( + "CONFIG_GET_INNER_USES_PHYSICAL_QUEUE_NOT_SET", interaction.locale + ) + ) + image_text = ( self.bot.get_locale( "CONFIG_GET_INNER_IMAGES_IN_SUGGESTIONS_SET", interaction.locale @@ -472,7 +528,9 @@ async def send_full_config(self, interaction: disnake.GuildCommandInteraction): f"Log channel: {log_channel}\nDm responses: I {dm_responses} DM users on actions such as suggest\n" f"Suggestion threads: {threads}\nKeep Logs: {keep_logs}\nAnonymous suggestions: {anon}\n" f"Automatic thread archiving: {auto_archive_threads}\nSuggestions queue: {suggestions_queue}\n" - f"Images in suggestions: {images}\nAnonymous resolutions: {anonymous_resolutions}", + f"Physical queue: {physical_queue}\nImages in suggestions: {images}\n" + f"Anonymous resolutions: {anonymous_resolutions}\n" + f"Queue channel: {queue_channel}\nQueue rejection channel: {queue_rejection_channel}", color=self.bot.colors.embed_color, timestamp=self.bot.state.now, ).set_author(name=guild.name, icon_url=icon_url) diff --git a/suggestions/locales/en_US.json b/suggestions/locales/en_US.json index 9a92945..02b2912 100644 --- a/suggestions/locales/en_US.json +++ b/suggestions/locales/en_US.json @@ -68,6 +68,10 @@ "CONFIG_GET_INNER_BASE_EMBED_DESCRIPTION": "Configuration for {}\n", "CONFIG_GET_INNER_PARTIAL_LOG_CHANNEL_SET": "Log channel: <#{}>", "CONFIG_GET_INNER_PARTIAL_LOG_CHANNEL_NOT_SET": "Not set", + "CONFIG_GET_INNER_PARTIAL_QUEUE_LOG_CHANNEL_SET": "Queue channel: <#{}>", + "CONFIG_GET_INNER_PARTIAL_QUEUE_LOG_CHANNEL_NOT_SET": "Not set", + "CONFIG_GET_INNER_PARTIAL_QUEUE_REJECTION_LOG_CHANNEL_SET": "Queue rejection channel: <#{}>", + "CONFIG_GET_INNER_PARTIAL_QUEUE_REJECTION_LOG_CHANNEL_NOT_SET": "Not set", "CONFIG_GET_INNER_PARTIAL_SUGGESTION_CHANNEL_SET": "Suggestion channel: <#{}>", "CONFIG_GET_INNER_PARTIAL_SUGGESTION_CHANNEL_NOT_SET": "Not set", "CONFIG_GET_INNER_PARTIAL_DM_RESPONSES_SET": "will", @@ -81,6 +85,8 @@ "CONFIG_GET_INNER_ANONYMOUS_SUGGESTIONS_SET": "can", "CONFIG_GET_INNER_ANONYMOUS_SUGGESTIONS_NOT_SET": "cannot", "CONFIG_GET_INNER_ANONYMOUS_SUGGESTIONS_MESSAGE": "This guild {} have anonymous suggestions.", + "CONFIG_GET_INNER_USES_PHYSICAL_QUEUE_SET": "This guild uses a virtual suggestions queue.", + "CONFIG_GET_INNER_USES_PHYSICAL_QUEUE_NOT_SET": "This guild uses a physical suggestions queue.", "CONFIG_GET_INNER_AUTO_ARCHIVE_THREADS_SET": "will", "CONFIG_GET_INNER_AUTO_ARCHIVE_THREADS_NOT_SET": "will not", "CONFIG_GET_INNER_AUTO_ARCHIVE_THREADS_MESSAGE": "I {} automatically archive threads created for suggestions.", From d7192f7c5fee96aac9b5f5e5eeec248fe398d1fb Mon Sep 17 00:00:00 2001 From: skelmis Date: Sun, 25 Feb 2024 19:26:41 +1300 Subject: [PATCH 09/12] fix: tests --- tests/test_interaction_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_interaction_handler.py b/tests/test_interaction_handler.py index 6c87ba8..e24a515 100644 --- a/tests/test_interaction_handler.py +++ b/tests/test_interaction_handler.py @@ -57,8 +57,8 @@ async def test_new_handler(bot: SuggestionsBot): async def test_fetch_handler(bot: SuggestionsBot): application_id = 123456789 - with pytest.raises(NonExistentEntry): - await InteractionHandler.fetch_handler(application_id, bot) + r_1 = await InteractionHandler.fetch_handler(application_id, bot) + assert r_1 is None mock = AsyncMock() mock.client = bot From 8cc9f41b6e8cd2cc5faca98f2d6c04840b8ecba1 Mon Sep 17 00:00:00 2001 From: skelmis Date: Sun, 25 Feb 2024 21:06:52 +1300 Subject: [PATCH 10/12] chore: make button styling more obvious --- suggestions/cogs/suggestion_cog.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/suggestions/cogs/suggestion_cog.py b/suggestions/cogs/suggestion_cog.py index 64d3545..7c06a02 100644 --- a/suggestions/cogs/suggestion_cog.py +++ b/suggestions/cogs/suggestion_cog.py @@ -7,7 +7,7 @@ import disnake from commons.caching import NonExistentEntry from bot_base.wraps import WrappedChannel -from disnake import Guild +from disnake import Guild, ButtonStyle from disnake.ext import commands, components from suggestions import checks, Stats, ErrorCode @@ -257,10 +257,12 @@ async def suggest( disnake.ui.Button( label="Approve queued suggestion", custom_id=await self.queue_approve.build_custom_id(), + style=ButtonStyle.green, ), disnake.ui.Button( label="Reject queued suggestion", custom_id=await self.queue_reject.build_custom_id(), + style=ButtonStyle.danger, ), ], ) From e672b6712fcf71ad3b8865a219310618a41928d9 Mon Sep 17 00:00:00 2001 From: skelmis Date: Tue, 27 Feb 2024 18:32:39 +1300 Subject: [PATCH 11/12] chore: rename physical queue to channel queue --- suggestions/cogs/guild_config_cog.py | 25 ++++++++++++++++++------- suggestions/core/suggestions_queue.py | 2 +- suggestions/locales/en_US.json | 6 +++--- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/suggestions/cogs/guild_config_cog.py b/suggestions/cogs/guild_config_cog.py index c55d0a2..7b90c55 100644 --- a/suggestions/cogs/guild_config_cog.py +++ b/suggestions/cogs/guild_config_cog.py @@ -192,7 +192,7 @@ async def get( "Suggestions queue", "Images in suggestions", "Anonymous resolutions", - "Using physical queue", + "Using channel queue", "Queue channel", "Queue rejection channel", ], @@ -373,6 +373,17 @@ async def get( embed.description += text + elif config == "Using channel queue": + locale_string = ( + "CONFIG_GET_INNER_USES_PHYSICAL_QUEUE_NOT_SET" + if guild_config.virtual_suggestion_queue + else "CONFIG_GET_INNER_USES_PHYSICAL_QUEUE_SET" + ) + + text = self.bot.get_localized_string(locale_string, interaction) + + embed.description += text + else: raise InvalidGuildConfigOption @@ -471,11 +482,11 @@ async def send_full_config(self, interaction: disnake.GuildCommandInteraction): physical_queue = ( self.bot.get_locale( - "CONFIG_GET_INNER_USES_PHYSICAL_QUEUE_SET", interaction.locale + "CONFIG_GET_INNER_USES_PHYSICAL_QUEUE_NOT_SET", interaction.locale ) if guild_config.virtual_suggestion_queue else self.bot.get_locale( - "CONFIG_GET_INNER_USES_PHYSICAL_QUEUE_NOT_SET", interaction.locale + "CONFIG_GET_INNER_USES_PHYSICAL_QUEUE_SET", interaction.locale ) ) @@ -528,7 +539,7 @@ async def send_full_config(self, interaction: disnake.GuildCommandInteraction): f"Log channel: {log_channel}\nDm responses: I {dm_responses} DM users on actions such as suggest\n" f"Suggestion threads: {threads}\nKeep Logs: {keep_logs}\nAnonymous suggestions: {anon}\n" f"Automatic thread archiving: {auto_archive_threads}\nSuggestions queue: {suggestions_queue}\n" - f"Physical queue: {physical_queue}\nImages in suggestions: {images}\n" + f"Channel queue: {physical_queue}\nImages in suggestions: {images}\n" f"Anonymous resolutions: {anonymous_resolutions}\n" f"Queue channel: {queue_channel}\nQueue rejection channel: {queue_rejection_channel}", color=self.bot.colors.embed_color, @@ -779,10 +790,10 @@ async def anonymous_resolutions_disable( ) @config.sub_command_group() - async def use_physical_queue(self, interaction: disnake.GuildCommandInteraction): + async def use_channel_queue(self, interaction: disnake.GuildCommandInteraction): pass - @use_physical_queue.sub_command(name="enable") + @use_channel_queue.sub_command(name="enable") async def use_physical_queue_enable( self, interaction: disnake.GuildCommandInteraction ): @@ -798,7 +809,7 @@ async def use_physical_queue_enable( self.stats.type.GUILD_PHYSICAL_QUEUE_ENABLE, ) - @use_physical_queue.sub_command(name="disable") + @use_channel_queue.sub_command(name="disable") async def use_physical_queue_disable( self, interaction: disnake.GuildCommandInteraction ): diff --git a/suggestions/core/suggestions_queue.py b/suggestions/core/suggestions_queue.py index fda3089..0bbab2a 100644 --- a/suggestions/core/suggestions_queue.py +++ b/suggestions/core/suggestions_queue.py @@ -117,7 +117,7 @@ async def resolve_queued_suggestion( try: guild_config: GuildConfig = await GuildConfig.from_id(guild_id, self.state) - # If sent to physical queue, delete it + # If sent to channel queue, delete it if not queued_suggestion.is_in_virtual_queue: chan = await self.state.fetch_channel(queued_suggestion.channel_id) msg = await chan.fetch_message(queued_suggestion.message_id) diff --git a/suggestions/locales/en_US.json b/suggestions/locales/en_US.json index 02b2912..c5a75d2 100644 --- a/suggestions/locales/en_US.json +++ b/suggestions/locales/en_US.json @@ -85,8 +85,8 @@ "CONFIG_GET_INNER_ANONYMOUS_SUGGESTIONS_SET": "can", "CONFIG_GET_INNER_ANONYMOUS_SUGGESTIONS_NOT_SET": "cannot", "CONFIG_GET_INNER_ANONYMOUS_SUGGESTIONS_MESSAGE": "This guild {} have anonymous suggestions.", - "CONFIG_GET_INNER_USES_PHYSICAL_QUEUE_SET": "This guild uses a virtual suggestions queue.", - "CONFIG_GET_INNER_USES_PHYSICAL_QUEUE_NOT_SET": "This guild uses a physical suggestions queue.", + "CONFIG_GET_INNER_USES_PHYSICAL_QUEUE_SET": "This guild uses a physical suggestions queue.", + "CONFIG_GET_INNER_USES_PHYSICAL_QUEUE_NOT_SET": "This guild uses a virtual suggestions queue.", "CONFIG_GET_INNER_AUTO_ARCHIVE_THREADS_SET": "will", "CONFIG_GET_INNER_AUTO_ARCHIVE_THREADS_NOT_SET": "will not", "CONFIG_GET_INNER_AUTO_ARCHIVE_THREADS_MESSAGE": "I {} automatically archive threads created for suggestions.", @@ -132,6 +132,6 @@ "SUGGESTION_ID_DESCRIPTION": "The suggestions ID you wish to reference.", "USER_ID_NAME": "user_id", "USER_ID_DESCRIPTION": "The users discord id.", - "CONFIG_USE_PHYSICAL_QUEUE_ENABLE_INNER_MESSAGE": "If the suggestions queue is enabled, all new suggestions will go to a physical queue.", + "CONFIG_USE_PHYSICAL_QUEUE_ENABLE_INNER_MESSAGE": "If the suggestions queue is enabled, all new suggestions will go to a channel queue.", "CONFIG_USE_PHYSICAL_QUEUE_DISABLE_INNER_MESSAGE": "If the suggestions queue is enabled, all new suggestions will go to a virtual queue." } From 321497366df6863aaa1a9046252928d1496dc286 Mon Sep 17 00:00:00 2001 From: skelmis Date: Fri, 1 Mar 2024 20:18:43 +1300 Subject: [PATCH 12/12] chore: fix wording --- suggestions/locales/en_US.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suggestions/locales/en_US.json b/suggestions/locales/en_US.json index c5a75d2..52b457e 100644 --- a/suggestions/locales/en_US.json +++ b/suggestions/locales/en_US.json @@ -85,7 +85,7 @@ "CONFIG_GET_INNER_ANONYMOUS_SUGGESTIONS_SET": "can", "CONFIG_GET_INNER_ANONYMOUS_SUGGESTIONS_NOT_SET": "cannot", "CONFIG_GET_INNER_ANONYMOUS_SUGGESTIONS_MESSAGE": "This guild {} have anonymous suggestions.", - "CONFIG_GET_INNER_USES_PHYSICAL_QUEUE_SET": "This guild uses a physical suggestions queue.", + "CONFIG_GET_INNER_USES_PHYSICAL_QUEUE_SET": "This guild uses a channel based suggestions queue.", "CONFIG_GET_INNER_USES_PHYSICAL_QUEUE_NOT_SET": "This guild uses a virtual suggestions queue.", "CONFIG_GET_INNER_AUTO_ARCHIVE_THREADS_SET": "will", "CONFIG_GET_INNER_AUTO_ARCHIVE_THREADS_NOT_SET": "will not",