Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

version 3.22 #75

Merged
merged 11 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
## Summary

<!-- What is this pull request for? -->

## Checklist
Expand All @@ -14,4 +13,4 @@
- [ ] New errors have been updated on ``stats.suggestions.gg``
- [ ] Guild config method names aren't duplicated
- [ ] New localizations have been added
- [ ] Documentation on ``docs.suggestions.gg`` has been updated
- [ ] Documentation on ``docs.suggestions.gg`` has been updated
3 changes: 1 addition & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
name: "Pytest"
on:
pull_request:
branches: [ master ]
push:
branches: [ master ]
branches: master

jobs:
run_tests:
Expand Down
19 changes: 19 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import alaric
from alaric import Cursor
from dotenv import load_dotenv
from logoo import PrimaryLogger

import suggestions

Expand All @@ -29,6 +30,9 @@
shard_logger = logging.getLogger("disnake.shard")
shard_logger.setLevel(logging.WARNING)

httpx_logger = logging.getLogger("httpx")
httpx_logger.setLevel(logging.WARNING)

suggestions_logger = logging.getLogger("suggestions")
suggestions_logger.setLevel(logging.DEBUG)
member_stats_logger = logging.getLogger("suggestions.objects.stats.member_stats")
Expand All @@ -40,6 +44,21 @@ async def run_bot():
log = logging.getLogger(__name__)
bot = await suggestions.create_bot()

logger: PrimaryLogger = PrimaryLogger(
__name__,
base_url="https://logs.suggestions.gg",
org="default",
stream="prod_bot" if bot.is_prod else "test_bot",
username=os.environ["LOGOO_USER"],
password=os.environ["LOGOO_PASSWORD"],
poll_time=15,
global_metadata={
"cluster": bot.cluster_id,
"bot_version": bot.version,
},
)
await logger.start_consumer()

# Make sure we don't shutdown due to a previous shutdown request
cursor: Cursor = (
Cursor.from_document(bot.db.cluster_shutdown_requests)
Expand Down
9 changes: 4 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
The Suggestions Rewrite
The Suggestions Bot
---

A simplistic bot written by @skelmis utilising Python in
order to get the suggestion's bot back on its feet while
also providing breathing room for @acollierr17 to further
work on the future of this bot.
This bot represents the code base for [suggestions.gg](https://suggestions.gg).

While it is open source we do not provide support for self-hosting.
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,6 @@ typing_extensions==4.3.0
websockets==10.4
yarl==1.7.2
zonis==1.2.5
types-aiobotocore==2.11.2
aiobotocore==2.11.2
logoo==1.2.0
100 changes: 91 additions & 9 deletions suggestions/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
from alaric import Cursor
from bot_base.wraps import WrappedChannel
from cooldowns import CallableOnCooldown
from disnake import Locale, LocalizationKeyError, GatewayParams
from disnake import Locale, LocalizationKeyError
from disnake.ext import commands
from bot_base import BotBase, BotContext, PrefixNotFound
from logoo import Logger

from suggestions import State, Colors, Emojis, ErrorCode, Garven
from suggestions.exceptions import (
Expand All @@ -36,19 +37,24 @@
QueueImbalance,
BlocklistedUser,
PartialResponse,
MissingQueueLogsChannel,
MissingPermissionsToAccessQueueChannel,
InvalidFileType,
)
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
from suggestions.zonis_routes import ZonisRoutes

log = logging.getLogger(__name__)
logger = Logger(__name__)


class SuggestionsBot(commands.AutoShardedInteractionBot, BotBase):
def __init__(self, *args, **kwargs):
self.version: str = "Public Release 3.21"
self.version: str = "Public Release 3.22"
self.main_guild_id: int = 601219766258106399
self.legacy_beta_role_id: int = 995588041991274547
self.automated_beta_role_id: int = 998173237282361425
Expand Down Expand Up @@ -185,7 +191,7 @@ def error_embed(
text=f"Error code {error_code.value} | Cluster ID {self.cluster_id}"
)

log.debug("Encountered %s", error_code.name)
logger.debug("Encountered %s", error_code.name)
elif error:
embed.set_footer(text=f"Error ID {error.id}")

Expand Down Expand Up @@ -239,11 +245,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

Expand Down Expand Up @@ -379,6 +390,30 @@ 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_channel`",
error_code=ErrorCode.MISSING_QUEUE_LOG_CHANNEL,
error=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(
Expand Down Expand Up @@ -447,6 +482,18 @@ async def on_slash_command_error(
ephemeral=True,
)

elif isinstance(exception, InvalidFileType):
return await interaction.send(
embed=self.error_embed(
"Invalid file type",
"The file you attempted to upload is not an accepted type.\n\n"
"If you believe this is an error please reach out to us via our support discord.",
error_code=ErrorCode.INVALID_FILE_TYPE,
error=error,
),
ephemeral=True,
)

elif isinstance(exception, ConfiguredChannelNoLongerExists):
return await interaction.send(
embed=self.error_embed(
Expand Down Expand Up @@ -484,6 +531,14 @@ async def on_slash_command_error(

elif isinstance(exception, disnake.NotFound):
log.debug("disnake.NotFound: %s", exception.text)
logger.debug(
"disnake.NotFound: %s",
exception.text,
extra_metadata={
"guild_id": interaction.guild_id,
"author_id": interaction.author.id,
},
)
gid = interaction.guild_id if interaction.guild_id else None
await interaction.send(
embed=self.error_embed(
Expand All @@ -499,6 +554,14 @@ async def on_slash_command_error(

elif isinstance(exception, disnake.Forbidden):
log.debug("disnake.Forbidden: %s", exception.text)
logger.debug(
"disnake.Forbidden: %s",
exception.text,
extra_metadata={
"guild_id": interaction.guild_id,
"author_id": interaction.author.id,
},
)
await interaction.send(
embed=self.error_embed(
exception.text,
Expand Down Expand Up @@ -528,9 +591,17 @@ async def on_slash_command_error(
log.debug(
"disnake.HTTPException: Interaction has already been acknowledged"
)
logger.debug(
"disnake.HTTPException: Interaction has already been acknowledged"
)
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(
Expand Down Expand Up @@ -743,7 +814,7 @@ async def process_update_bot_listings():
json=body,
) as r:
if r.status != 200:
log.warning("%s", r.text)
logger.warning("%s", r.text)

log.debug("Updated bot listings")
await self.sleep_with_condition(
Expand Down Expand Up @@ -790,7 +861,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(
Expand Down Expand Up @@ -830,11 +901,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
Expand Down Expand Up @@ -885,6 +960,9 @@ async def inner():
await self.sleep_with_condition(60, lambda: self.state.is_closing)
except (aiohttp.ClientConnectorError, ConnectionRefusedError):
log.warning("push_status failed to connect, retrying in 10 seconds")
logger.warning(
"push_status failed to connect, retrying in 10 seconds"
)
await self.sleep_with_condition(10, lambda: self.state.is_closing)
except Exception as e:
if not self.is_prod:
Expand All @@ -896,6 +974,10 @@ async def inner():
"Status update failed: %s",
tb,
)
logger.error(
"Status update failed: %s",
tb,
)
await self.garven.notify_devs(
title="Status page ping error",
description=tb,
Expand Down
38 changes: 31 additions & 7 deletions suggestions/clunk2/edits.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
from __future__ import annotations

import asyncio
import logging
from typing import TYPE_CHECKING

import disnake
from logoo import Logger

from suggestions.low_level import MessageEditing
from suggestions.objects import Suggestion

if TYPE_CHECKING:
from suggestions import SuggestionsBot

log = logging.getLogger(__name__)
logger = Logger(__name__)

pending_edits: set[str] = set()

Expand All @@ -24,26 +24,50 @@ async def update_suggestion_message(
time_after: float = 10,
):
if suggestion.suggestion_id in pending_edits:
log.debug("Ignoring already existing item %s", suggestion.suggestion_id)
logger.debug(
"Ignoring already existing item %s",
suggestion.suggestion_id,
extra_metadata={
"guild_id": suggestion.guild_id,
"suggestion_id": suggestion.suggestion_id,
},
)
return

pending_edits.add(suggestion.suggestion_id)
await asyncio.sleep(time_after)
if suggestion.channel_id is None or suggestion.message_id is None:
log.debug(
logger.debug(
"Suggestion %s had a NoneType by the time it was to be edited channel_id=%s, message_id=%s",
suggestion.suggestion_id,
suggestion.channel_id,
suggestion.message_id,
extra_metadata={
"guild_id": suggestion.guild_id,
"suggestion_id": suggestion.suggestion_id,
},
)
pending_edits.discard(suggestion.suggestion_id)
return

# We do this to avoid a race condition where the suggestion may have
# had a value modified between when it was added to the edit queue
# and the time at which it was actually edited
up_to_date_suggestion = await bot.state.suggestions_db.find(suggestion)
try:
await MessageEditing(
bot, channel_id=suggestion.channel_id, message_id=suggestion.message_id
).edit(embed=await suggestion.as_embed(bot))
bot,
channel_id=up_to_date_suggestion.channel_id,
message_id=up_to_date_suggestion.message_id,
).edit(embed=await up_to_date_suggestion.as_embed(bot))
except (disnake.HTTPException, disnake.NotFound):
log.error("Failed to update suggestion %s", suggestion.suggestion_id)
logger.error(
"Failed to update suggestion %s",
suggestion.suggestion_id,
extra_metadata={
"guild_id": suggestion.guild_id,
"suggestion_id": suggestion.suggestion_id,
},
)

pending_edits.discard(suggestion.suggestion_id)
3 changes: 3 additions & 0 deletions suggestions/codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ class ErrorCode(IntEnum):
QUEUE_IMBALANCE = 20
MISSING_QUEUE_CHANNEL = 21
BLOCKLISTED_USER = 22
MISSING_QUEUE_LOG_CHANNEL = 23
MISSING_PERMISSIONS_IN_QUEUE_CHANNEL = 24
INVALID_FILE_TYPE = 25

@classmethod
def from_value(cls, value: int) -> ErrorCode:
Expand Down
Loading
Loading