Files
croccybot/src/babel/cog.py
2023-06-06 14:27:57 +03:00

257 lines
9.5 KiB
Python

"""
Babel Cog.
Calculates and sets current locale before command runs (via check_once).
Also defines the relevant guild and user settings for localisation.
"""
from typing import Optional
import discord
from discord.ext import commands as cmds
from discord import app_commands as appcmds
from discord.ui.button import ButtonStyle
from meta import LionBot, LionCog, LionContext
from meta.errors import UserInputError
from utils.ui import AButton, AsComponents
from wards import low_management_ward
from .translator import ctx_locale, ctx_translator, SOURCE_LOCALE
from . import babel
from .enums import locale_names
from .settings import LocaleSettings
from .settingui import LocaleSettingUI
_ = babel._
_p = babel._p
class BabelCog(LionCog):
depends = {'CoreCog'}
def __init__(self, bot: LionBot):
self.bot = bot
self.settings = LocaleSettings()
self.t = self.bot.translator.t
async def cog_load(self):
if not self.bot.core:
raise ValueError("CoreCog must be loaded first!")
self.bot.core.guild_config.register_model_setting(LocaleSettings.ForceLocale)
self.bot.core.guild_config.register_model_setting(LocaleSettings.GuildLocale)
self.bot.core.user_config.register_model_setting(LocaleSettings.UserLocale)
configcog = self.bot.get_cog('ConfigCog')
self.crossload_group(self.configure_group, configcog.configure_group)
userconfigcog = self.bot.get_cog('UserConfigCog')
self.crossload_group(self.userconfig_group, userconfigcog.userconfig_group)
async def cog_unload(self):
pass
async def get_user_locale(self, userid):
"""
Fetch the best locale we can guess for this userid.
"""
data = await self.bot.core.data.User.fetch(userid)
if data:
return data.locale or data.locale_hint or SOURCE_LOCALE
else:
return SOURCE_LOCALE
async def bot_check_once(self, ctx: LionContext): # type: ignore # Type checker doesn't understand coro checks
"""
Calculate and inject the current locale before the command begins.
Locale resolution is calculated as follows:
If the guild has force_locale enabled, and a locale set,
then the guild's locale will be used.
Otherwise, the priority is
user_locale -> command_locale -> user_locale_hint -> guild_locale -> default_locale
"""
locale = None
if ctx.guild:
forced = ctx.lguild.config.get('force_locale').value
guild_locale = ctx.lguild.config.get('guild_locale').value
if forced:
locale = guild_locale
locale = locale or ctx.luser.config.get('user_locale').value
if ctx.interaction:
locale = locale or ctx.interaction.locale.value
if ctx.guild:
locale = locale or guild_locale
locale = locale or SOURCE_LOCALE
ctx_locale.set(locale)
ctx_translator.set(self.bot.translator)
return True
@LionCog.placeholder_group
@cmds.hybrid_group('configure', with_app_command=False)
async def configure_group(self, ctx: LionContext):
# Placeholder group method, not used.
pass
@configure_group.command(
name=_p('cmd:configure_language', "language"),
description=_p('cmd:configure_language|desc',
"Configure the default language I will use in this server.")
)
@appcmds.choices(
force_language=[
appcmds.Choice(name=LocaleSettings.ForceLocale._outputs[True], value=1),
appcmds.Choice(name=LocaleSettings.ForceLocale._outputs[False], value=0),
]
)
@appcmds.describe(
language=LocaleSettings.GuildLocale._desc,
force_language=LocaleSettings.ForceLocale._desc
)
@appcmds.rename(
language=LocaleSettings.GuildLocale._display_name,
force_language=LocaleSettings.ForceLocale._display_name
)
@appcmds.guild_only() # Can be removed when attached as a subcommand
@appcmds.default_permissions(manage_guild=True)
@low_management_ward
async def cmd_configure_language(self, ctx: LionContext,
language: Optional[str] = None,
force_language: Optional[appcmds.Choice[int]] = None):
if not ctx.interaction:
# This command is not available as a text command
return
if not ctx.guild:
# This is impossible by decorators, but adding this guard for the type checker
return
t = self.t
# TODO: Setting group, and group setting widget
# We can attach the command to the setting group as an application command
# Then load it into the configure command group dynamically
lang_setting = await self.settings.GuildLocale.get(ctx.guild.id)
force_setting = await self.settings.ForceLocale.get(ctx.guild.id)
if language:
lang_data = await lang_setting._parse_string(ctx.guild.id, language)
if force_language is not None:
force_data = bool(force_language.value)
if force_language is not None and not (lang_data if language is not None else lang_setting.value):
# Setting force without having a language!
raise UserInputError(
t(_p(
'cmd:configure_language|error',
"You cannot enable `{force_setting}` without having a configured language!"
)).format(force_setting=t(LocaleSettings.ForceLocale.display_name))
)
# TODO: Really need simultaneous model writes, or batched writes
lines = []
if language:
lang_setting.data = lang_data
await lang_setting.write()
lines.append(lang_setting.update_message)
if force_language is not None:
force_setting.data = force_data
await force_setting.write()
lines.append(force_setting.update_message)
if lines:
result = '\n'.join(
f"{self.bot.config.emojis.tick} {line}" for line in lines
)
await ctx.reply(
embed=discord.Embed(
colour=discord.Colour.green(),
title=t(_p('cmd:configure_language|success', "Language settings updated!")),
description=result
)
)
if ctx.channel.id not in LocaleSettingUI._listening or not lines:
ui = LocaleSettingUI(self.bot, ctx.guild.id, ctx.channel.id)
await ui.run(ctx.interaction)
await ui.wait()
@LionCog.placeholder_group
@cmds.hybrid_group(name='my')
async def userconfig_group(self, ctx: LionContext):
pass
@userconfig_group.command(
name=_p('cmd:userconfig_language', "language"),
description=_p(
'cmd:userconfig_language|desc',
"Set your preferred interaction language."
)
)
@appcmds.rename(
language=_p('cmd:userconfig_language|param:language', "language")
)
@appcmds.describe(
language=_p(
'cmd:userconfig_language|param:language|desc',
"Which language do you want me to respond in?"
)
)
async def userconfig_language_cmd(self, ctx: LionContext, language: Optional[str] = None):
if not ctx.interaction:
return
t = self.bot.translator.t
setting = await self.settings.UserLocale.get(ctx.author.id)
if language:
new_data = await setting._parse_string(ctx.author.id, language)
await setting.interactive_set(new_data, ctx.interaction, ephemeral=True)
else:
embed = setting.embed
if setting.value:
@AButton(
label=t(_p('cmd:userconfig_language|button:reset|label', "Reset")),
style=ButtonStyle.red
)
async def reset_button(_press: discord.Interaction, pressed):
await _press.response.defer()
await setting.interactive_set(None, ctx.interaction, view=None)
view = AsComponents(reset_button)
else:
view = None
await ctx.reply(embed=embed, ephemeral=True, view=view)
@userconfig_language_cmd.autocomplete('language')
@cmd_configure_language.autocomplete('language')
async def acmpl_language(self, interaction: discord.Interaction, partial: str):
"""
Shared autocomplete for language options.
"""
t = self.bot.translator.t
supported = self.bot.translator.supported_locales
formatted = []
for locale in supported:
name = locale_names.get(locale, None)
if name:
localestr = f"{locale} ({t(name)})"
else:
localestr = locale
formatted.append((locale, localestr))
matching = {item for item in formatted if partial in item[1]}
if matching:
choices = [
appcmds.Choice(name=localestr, value=locale)
for locale, localestr in matching
]
else:
choices = [
appcmds.Choice(
name=t(_p(
'acmpl:language|no_match',
"No supported languages matching {partial}"
)).format(partial=partial),
value=partial
)
]
return choices