Skip to content

Commit

Permalink
refactor: Refactor Translator and LocaleStr (#86)
Browse files Browse the repository at this point in the history
* WIP

* WIP

* Modify commands

* Refactor LevelStr

* Some refactor

* Add back key to app commands locale str

* Fix search command invalid description

* Add aiocache

* Fix enum issues
  • Loading branch information
seriaati authored Jun 14, 2024
1 parent b3a47bd commit 042e973
Show file tree
Hide file tree
Showing 106 changed files with 1,476 additions and 2,197 deletions.
58 changes: 31 additions & 27 deletions hoyo_buddy/bot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@
from ..exceptions import NoAccountFoundError
from ..hoyo.clients.novel_ai import NAIClient
from ..utils import get_now
from .cache import LFUCache
from .command_tree import CommandTree
from .translator import AppCommandTranslator, LocaleStr, Translator
from .translator import AppCommandTranslator, EnumStr, LocaleStr, Translator

if TYPE_CHECKING:
import asyncio
from collections.abc import Sequence
from enum import StrEnum

import asyncpg
import git
Expand Down Expand Up @@ -98,6 +100,7 @@ def __init__(
self.pool = pool
self.executor = concurrent.futures.ProcessPoolExecutor()
self.config = config
self.cache = LFUCache()

self.autocomplete_choices: AutocompleteChoices = {}
"""[game][category][locale][item_name] -> item_id"""
Expand Down Expand Up @@ -154,12 +157,26 @@ async def dm_user(self, user_id: int, **kwargs: Any) -> discord.Message | None:

return message

@staticmethod
def get_error_app_command_choice(error_message: LocaleStr) -> app_commands.Choice[str]:
return app_commands.Choice(
name=error_message.to_app_command_locale_str(),
value="none",
)
def get_error_autocomplete(
self, error_message: LocaleStr, locale: discord.Locale
) -> list[app_commands.Choice[str]]:
return [
app_commands.Choice(
name=error_message.translate(self.translator, locale),
value="none",
)
]

def get_enum_autocomplete(
self, enums: Sequence[StrEnum], locale: discord.Locale, current: str
) -> list[discord.app_commands.Choice[str]]:
return [
discord.app_commands.Choice(
name=EnumStr(enum).translate(self.translator, locale), value=enum.value
)
for enum in enums
if current.lower() in EnumStr(enum).translate(self.translator, locale).lower()
]

async def get_account_autocomplete(
self,
Expand Down Expand Up @@ -195,26 +212,16 @@ async def get_account_autocomplete(

if not accounts:
if is_author:
return [
self.get_error_app_command_choice(
LocaleStr(
"You don't have any accounts yet. Add one with /accounts",
key="no_accounts_autocomplete_choice",
)
)
]
return [
self.get_error_app_command_choice(
LocaleStr(
"This user doesn't have any accounts yet",
key="user_no_accounts_autocomplete_choice",
)
return self.get_error_autocomplete(
LocaleStr(key="no_accounts_autocomplete_choice"), locale
)
]
return self.get_error_autocomplete(
LocaleStr(key="user_no_accounts_autocomplete_choice"), locale
)

return [
discord.app_commands.Choice(
name=f"{account if is_author else account.blurred_display} | {translator.translate(LocaleStr(account.game, warn_no_key=False), locale)}{' (✦)' if account.current else ''}",
name=f"{account if is_author else account.blurred_display} | {translator.translate(EnumStr(account.game), locale)}{' (✦)' if account.current else ''}",
value=f"{account.id}",
)
for account in accounts
Expand Down Expand Up @@ -272,10 +279,7 @@ def get_all_commands(self, locale: discord.Locale) -> dict[str, str]:
for cog in self.cogs.values():
for command in cog.walk_app_commands():
desc = (
LocaleStr(
command._locale_description.message,
**command._locale_description.extras,
)
LocaleStr(key=command._locale_description.message)
if command._locale_description is not None
else command.description
)
Expand Down
48 changes: 48 additions & 0 deletions hoyo_buddy/bot/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from collections import OrderedDict, defaultdict
from typing import Any

import aiocache


class LFUCache(aiocache.SimpleMemoryCache):
def __init__(self, maxsize: int = 1024, **kwargs: Any) -> None:
super().__init__(**kwargs)
self._maxsize = maxsize
self._frequency: defaultdict[Any, int] = defaultdict(int)
self._freq_list: OrderedDict[int, list[Any]] = OrderedDict()

def _update_freq(self, key: Any) -> None:
freq = self._frequency[key]
self._frequency[key] += 1

if freq in self._freq_list:
self._freq_list[freq].remove(key)
if not self._freq_list[freq]:
del self._freq_list[freq]

if self._frequency[key] not in self._freq_list:
self._freq_list[self._frequency[key]] = []
self._freq_list[self._frequency[key]].append(key)

async def _evict(self) -> None:
if len(self._cache) >= self._maxsize:
min_freq = next(iter(self._freq_list))
key_to_evict = self._freq_list[min_freq].pop(0)
if not self._freq_list[min_freq]:
del self._freq_list[min_freq]
await super().delete(key_to_evict)
del self._frequency[key_to_evict]

async def get(self, key: Any, default: Any = None) -> Any:
if await super().exists(key):
self._update_freq(key)
return await super().get(key, default)

async def set(self, key: Any, value: Any, ttl: int = 0) -> None:
await self._evict()
await super().set(key, value, ttl)
self._update_freq(key)

async def delete(self, key: Any) -> None:
await super().delete(key)
del self._frequency[key]
129 changes: 38 additions & 91 deletions hoyo_buddy/bot/error_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from ..enums import GeetestType
from ..exceptions import HoyoBuddyError, InvalidQueryError
from ..utils import get_now
from .translator import LocaleStr, Translator
from .translator import EnumStr, LocaleStr, Translator

if TYPE_CHECKING:
import discord
Expand All @@ -24,111 +24,66 @@

GENSHIN_ERROR_CONVERTER: dict[tuple[int, ...], dict[Literal["title", "description"], LocaleStr]] = {
(-5003,): {
"title": LocaleStr("Daily Check-In Reward Already Claimed", key="already_claimed_title"),
"description": LocaleStr("Come back tomorrow!", key="already_claimed_description"),
"title": LocaleStr(key="already_claimed_title"),
"description": LocaleStr(key="already_claimed_description"),
},
(-100, 10001, 10103, -1071): {
"title": LocaleStr("Invalid Cookies", key="invalid_cookies_title"),
"description": LocaleStr(
"Refresh your cookies by adding your accounts again using </accounts>",
key="invalid_cookies_description",
),
"title": LocaleStr(key="invalid_cookies_title"),
"description": LocaleStr(key="invalid_cookies_description"),
},
(-3205, -3102): {
"title": LocaleStr("Invalid Verification Code", key="invalid_verification_code_title"),
"description": LocaleStr(
"Please check the verification code and try again.",
key="invalid_verification_code_description",
),
"title": LocaleStr(key="invalid_verification_code_title"),
"description": LocaleStr(key="invalid_verification_code_description"),
},
(-3208, -3203, -3004): {
"title": LocaleStr("Invalid Email or Password", key="invalid_email_password_title"),
"description": LocaleStr(
"The email or password you provided is incorrect, please check and try again.",
key="invalid_email_password_description",
),
"title": LocaleStr(key="invalid_email_password_title"),
"description": LocaleStr(key="invalid_email_password_description"),
},
(-3206,): {
"title": LocaleStr(
"Verification Code Service Unavailable", key="verification_code_unavailable_title"
),
"description": LocaleStr(
"Please try again later", key="verification_code_unavailable_description"
),
"title": LocaleStr(key="verification_code_unavailable_title"),
"description": LocaleStr(key="verification_code_unavailable_description"),
},
(-3101, -1004): {
"title": LocaleStr("Action in Cooldown", key="action_in_cooldown_error_title"),
"title": LocaleStr(key="action_in_cooldown_error_title"),
"description": LocaleStr(
"Please try again at {available_time}.",
key="action_in_cooldown_error_message",
available_time=format_dt(get_now() + timedelta(minutes=1), "T"),
),
},
(-2017, -2018): {
"title": LocaleStr("Redemption code already claimed", key="redeem_code.already_claimed")
},
(-2001,): {
"title": LocaleStr("Redemption code expired", key="redeem_code.expired"),
},
(-1065, -2003, -2004, -2014): {
"title": LocaleStr("Invalid redemption code", key="redeem_code.invalid"),
},
(-2016,): {
"title": LocaleStr("Code redemption in cooldown", key="redeem_code.cooldown"),
},
(-2017, -2018): {"title": LocaleStr(key="redeem_code.already_claimed")},
(-2001,): {"title": LocaleStr(key="redeem_code.expired")},
(-1065, -2003, -2004, -2014): {"title": LocaleStr(key="redeem_code.invalid")},
(-2016,): {"title": LocaleStr(key="redeem_code.cooldown")},
(10102,): {
"title": LocaleStr("Data is not Public", key="data_not_public.title"),
"description": LocaleStr(
"Enable the data sharing option in your battle chronicle settings\nhttps://raw.githubusercontent.com/seriaati/hoyo-buddy/assets/DataNotPublicTutorial.gif",
key="data_not_public.description",
),
},
(-2021, -2011): {
"title": LocaleStr("Adventure rank too low", key="redeem_code.ar_too_low"),
"title": LocaleStr(key="data_not_public.title"),
"description": LocaleStr(key="data_not_public.description"),
},
(-2021, -2011): {"title": LocaleStr(key="redeem_code.ar_too_low")},
(30001,): {
"title": LocaleStr("No need for geetest", key="geetest.no_need"),
"description": LocaleStr(
"You never needed to do a geetest! What are you here for?!",
key="geetest.no_need.description",
),
"title": LocaleStr(key="geetest.no_need"),
"description": LocaleStr(key="geetest.no_need.description"),
},
tuple(genshin.constants.GEETEST_RETCODES): {
"title": LocaleStr("Geetest Verification Required", key="geetest.required"),
"title": LocaleStr(key="geetest.required"),
"description": LocaleStr(
"Use the </geetest> command, choose the account that triggered the geetest, and choose `{geetest_type}` to complete the verification",
geetest_type=LocaleStr(GeetestType.REALTIME_NOTES.value, warn_no_key=False),
geetest_type=EnumStr(GeetestType.REALTIME_NOTES),
key="geetest.required.description",
),
},
(-1,): {
"title": LocaleStr("Game is Under Maintenance", key="game_maintenance_title"),
},
(-1,): {"title": LocaleStr(key="game_maintenance_title")},
# Below are custom retcodes for Hoyo Buddy, they don't exist in Hoyo's API
(999,): {
"title": LocaleStr("Cookie Token Expired", key="redeeem_code.cookie_token_expired_title"),
"description": LocaleStr(
"Refresh your cookie token by adding your accounts again using </accounts>.\n"
"If you use the email and password method to add your accounts, cookie token can be refreshed automatically.",
key="redeeem_code.cookie_token_expired_description",
),
"title": LocaleStr(key="redeeem_code.cookie_token_expired_title"),
"description": LocaleStr(key="redeeem_code.cookie_token_expired_description"),
},
(1000,): {
"title": LocaleStr(
"Failed to Refresh Cookie Token", key="redeeem_code.cookie_token_refresh_failed_title"
),
"description": LocaleStr(
"It is likely that you have changed your account's password since the last time you add your accounts.\n"
"Please add your accounts again using </accounts> with the email and password method.",
key="redeeem_code.cookie_token_refresh_failed_description",
),
"title": LocaleStr(key="redeeem_code.cookie_token_refresh_failed_title"),
"description": LocaleStr(key="redeeem_code.cookie_token_refresh_failed_description"),
},
(-9999,): {
"title": LocaleStr("Geetest Verification Required", key="geetest.required"),
"title": LocaleStr(key="geetest.required"),
"description": LocaleStr(
"Use the </geetest> command, choose the account that triggered the geetest, and choose `{geetest_type}` to complete the verification",
geetest_type=LocaleStr(GeetestType.DAILY_CHECKIN.value, warn_no_key=False),
key="geetest.required.description",
geetest_type=EnumStr(GeetestType.DAILY_CHECKIN), key="geetest.required.description"
),
},
}
Expand All @@ -139,20 +94,16 @@
dict[Literal["title", "description"], LocaleStr],
] = {
enka_errors.PlayerDoesNotExistError: {
"title": LocaleStr("Player Does Not Exist", key="player_not_found_title"),
"description": LocaleStr(
"Please check the provided UID", key="player_not_found_description"
),
"title": LocaleStr(key="player_not_found_title"),
"description": LocaleStr(key="player_not_found_description"),
},
enka_errors.GameMaintenanceError: {
"title": LocaleStr("Game is Under Maintenance", key="game_maintenance_title"),
"description": LocaleStr("Please try again later", key="game_maintenance_description"),
"title": LocaleStr(key="game_maintenance_title"),
"description": LocaleStr(key="game_maintenance_description"),
},
enka_errors.WrongUIDFormatError: {
"title": LocaleStr("Invalid UID Format", key="invalid_uid_format_title"),
"description": LocaleStr(
"UID must be a string of 9 digits", key="invalid_uid_format_description"
),
"title": LocaleStr(key="invalid_uid_format_title"),
"description": LocaleStr(key="invalid_uid_format_description"),
},
}

Expand Down Expand Up @@ -198,13 +149,9 @@ def get_error_embed(
embed = ErrorEmbed(
locale,
translator,
title=LocaleStr("An Error Occurred", key="error_title"),
title=LocaleStr(key="error_title"),
description=description,
)
embed.set_footer(
text=LocaleStr(
"Please report this error to the developer via /feedback", key="error_footer"
)
)
embed.set_footer(text=LocaleStr(key="error_footer"))

return embed, recognized
Loading

0 comments on commit 042e973

Please sign in to comment.