From 7b18a7938dca5d2b5917cae6573968d73c58c0fd Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 14 Sep 2023 01:52:23 +0300 Subject: [PATCH 01/25] fix(ranks): Correct overview note creation. --- src/modules/ranks/ui/overview.py | 55 ++++++++++++++++---------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/src/modules/ranks/ui/overview.py b/src/modules/ranks/ui/overview.py index cd11e041..3562bfba 100644 --- a/src/modules/ranks/ui/overview.py +++ b/src/modules/ranks/ui/overview.py @@ -380,34 +380,6 @@ class RankOverviewUI(MessageUI): ] or [[]] lines = line_blocks[self.pagen] desc = '\n'.join(reversed(lines)) - - # Add note about season start - note_name = t(_p( - 'ui:rank_overview|embed|field:note|name', - "Note" - )) - season_start = self.lguild.data.season_start - if season_start: - season_str = t(_p( - 'ui:rank_overview|embed|field:note|value:with_season', - "Ranks are determined by activity since {timestamp}." - )).format( - timestamp=discord.utils.format_dt(season_start) - ) - else: - season_str = t(_p( - 'ui:rank_overview|embed|field:note|value:without_season', - "Ranks are determined by *all-time* statistics.\n" - "To reward ranks from a later time (e.g. to have monthly/quarterly/yearly ranks) " - "set the `season_start` with {stats_cmd}" - )).format(stats_cmd=self.bot.core.mention_cmd('configure statistics')) - if self.rank_type is RankType.VOICE: - addendum = t(_p( - 'ui:rank_overview|embed|field:note|value|voice_addendum', - "Also note that ranks will only be updated when a member leaves a tracked voice channel! " - "Use the **Refresh Member Ranks** button below to update all members manually." - )) - season_str = '\n'.join((season_str, addendum)) else: # No ranks, give hints about adding ranks desc = t(_p( @@ -439,6 +411,33 @@ class RankOverviewUI(MessageUI): description=desc ) if show_note: + # Add note about season start + note_name = t(_p( + 'ui:rank_overview|embed|field:note|name', + "Note" + )) + season_start = self.lguild.data.season_start + if season_start: + season_str = t(_p( + 'ui:rank_overview|embed|field:note|value:with_season', + "Ranks are determined by activity since {timestamp}." + )).format( + timestamp=discord.utils.format_dt(season_start) + ) + else: + season_str = t(_p( + 'ui:rank_overview|embed|field:note|value:without_season', + "Ranks are determined by *all-time* statistics.\n" + "To reward ranks from a later time (e.g. to have monthly/quarterly/yearly ranks) " + "set the `season_start` with {stats_cmd}" + )).format(stats_cmd=self.bot.core.mention_cmd('configure statistics')) + if self.rank_type is RankType.VOICE: + addendum = t(_p( + 'ui:rank_overview|embed|field:note|value|voice_addendum', + "Also note that ranks will only be updated when a member leaves a tracked voice channel! " + "Use the **Refresh Member Ranks** button below to update all members manually." + )) + season_str = '\n'.join((season_str, addendum)) embed.add_field( name=note_name, value=season_str, From 7473f88e1d90470a5497e2e3332bd271d7b2ed96 Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 14 Sep 2023 15:11:02 +0300 Subject: [PATCH 02/25] fix(stats): Fix typo in lb chunking. --- src/modules/statistics/cog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/statistics/cog.py b/src/modules/statistics/cog.py index 50086dff..d572f5e2 100644 --- a/src/modules/statistics/cog.py +++ b/src/modules/statistics/cog.py @@ -90,7 +90,7 @@ class StatsCog(LionCog): )).format(loading=self.bot.config.emojis.loading), timestamp=utc_now(), ) - await ctx.interaction.response(embed=waiting_embed) + await ctx.interaction.response.send_message(embed=waiting_embed) await ctx.guild.chunk() else: await ctx.interaction.response.defer(thinking=True) From a27e07be9719a56f5fea02c9c3f3f94cd4318364 Mon Sep 17 00:00:00 2001 From: Conatum Date: Mon, 18 Sep 2023 07:55:47 +0300 Subject: [PATCH 03/25] fix (member_admin): Change panel name. --- src/modules/member_admin/settingui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/member_admin/settingui.py b/src/modules/member_admin/settingui.py index cf08a161..91e0e93e 100644 --- a/src/modules/member_admin/settingui.py +++ b/src/modules/member_admin/settingui.py @@ -210,7 +210,7 @@ class MemberAdminUI(ConfigUI): t = self.bot.translator.t title = t(_p( 'ui:memberadmin|embed|title', - "Member Admin Configuration Panel" + "Greetings and Initial Roles Panel" )) embed = discord.Embed( title=title, From c63027f20e4c8c2660adbd52ea7909a4d1e6bab4 Mon Sep 17 00:00:00 2001 From: Conatum Date: Mon, 18 Sep 2023 08:56:57 +0300 Subject: [PATCH 04/25] fix(logging): Split warning and error logs. --- config/example-bot.conf | 6 ++++-- src/bot.py | 2 +- src/meta/LionContext.py | 1 + src/meta/config.py | 7 +++++-- src/meta/logger.py | 22 ++++++++++++++++++---- 5 files changed, 29 insertions(+), 9 deletions(-) diff --git a/config/example-bot.conf b/config/example-bot.conf index a5195d74..6ed72e6b 100644 --- a/config/example-bot.conf +++ b/config/example-bot.conf @@ -19,8 +19,10 @@ gem_transaction = log_file = bot.log general_log = -error_log = -critical_log = +error_log = %(general_log) +critical_log = %(general_log) +warning_log = %(general_log) +warning_prefix = error_prefix = critical_prefix = diff --git a/src/bot.py b/src/bot.py index a12ce61e..1e47a9e4 100644 --- a/src/bot.py +++ b/src/bot.py @@ -21,7 +21,7 @@ for name in conf.config.options('LOGGING_LEVELS', no_defaults=True): logging.getLogger(name).setLevel(conf.logging_levels[name]) -setup_main_logger() +logging_queue = setup_main_logger() logger = logging.getLogger(__name__) diff --git a/src/meta/LionContext.py b/src/meta/LionContext.py index 64746a5e..cef6754f 100644 --- a/src/meta/LionContext.py +++ b/src/meta/LionContext.py @@ -35,6 +35,7 @@ FlatContext = namedtuple( 'interaction', 'guild', 'author', + 'channel', 'alias', 'prefix', 'failed') diff --git a/src/meta/config.py b/src/meta/config.py index 621de6f6..58eb6bfa 100644 --- a/src/meta/config.py +++ b/src/meta/config.py @@ -3,7 +3,7 @@ import configparser as cfgp from .args import args -shard_number = args.shard or 0 +shard_number = args.shard class configEmoji(PartialEmoji): __slots__ = ('fallback',) @@ -87,7 +87,10 @@ class Conf: "emoji": configEmoji.from_str, } ) - self.config.read(configfile) + + with open(configfile) as conff: + # Opening with read_file mainly to ensure the file exists + self.config.read_file(conff) self.section_name = section_name if section_name in self.config else 'DEFAULT' diff --git a/src/meta/logger.py b/src/meta/logger.py index ddd7121a..b2464088 100644 --- a/src/meta/logger.py +++ b/src/meta/logger.py @@ -188,6 +188,14 @@ class LessThanFilter(logging.Filter): # non-zero return means we log this message return 1 if record.levelno < self.max_level else 0 +class ExactLevelFilter(logging.Filter): + def __init__(self, target_level, name=""): + super().__init__(name) + self.target_level = target_level + + def filter(self, record): + return (record.levelno == self.target_level) + class ThreadFilter(logging.Filter): def __init__(self, thread_name): @@ -234,7 +242,6 @@ class ContextInjection(logging.Filter): logging_handler_out = logging.StreamHandler(sys.stdout) logging_handler_out.setLevel(logging.DEBUG) logging_handler_out.setFormatter(log_fmt) -logging_handler_out.addFilter(LessThanFilter(logging.WARNING)) logging_handler_out.addFilter(ContextInjection()) logger.addHandler(logging_handler_out) log_logger.addHandler(logging_handler_out) @@ -363,14 +370,15 @@ class WebHookHandler(logging.StreamHandler): return except BucketFull: logger.warning( - f"Live logging webhook {self.webhook.id} going too fast! " - "Ignoring records until rate slows down." + "Can't keep up! " + "Ignoring records on live-logger {self.webhook.id}." ) self.ignored += 1 return else: if self.ignored > 0: logger.warning( + "Can't keep up! " f"{self.ignored} live logging records on webhook {self.webhook.id} skipped, continuing." ) self.ignored = 0 @@ -392,9 +400,15 @@ if webhook := conf.logging['general_log']: handler = WebHookHandler(webhook, batch=True) handlers.append(handler) +if webhook := conf.logging['warning_log']: + handler = WebHookHandler(webhook, prefix=conf.logging['warning_prefix'], batch=True) + handler.addFilter(ExactLevelFilter(logging.WARNING)) + handler.setLevel(logging.WARNING) + handlers.append(handler) + if webhook := conf.logging['error_log']: handler = WebHookHandler(webhook, prefix=conf.logging['error_prefix'], batch=True) - handler.setLevel(logging.WARNING) + handler.setLevel(logging.ERROR) handlers.append(handler) if webhook := conf.logging['critical_log']: From 1946077ded1a6d9c0fda29fa9fd3890ae985d0dc Mon Sep 17 00:00:00 2001 From: JetRaidz Date: Tue, 19 Sep 2023 18:32:29 +1200 Subject: [PATCH 05/25] Fix help command not working in DMs The `help` application command now functions as expected in DMs. This shows both member-level and admin-level commands. - Fixed `shop open` appearing in the DM slash command list - Fixed `support_guild` not being included in `example-bot.conf` - The following wards now require the `Guild` context to be passed to them: - `low_management` - `high_management` --- config/example-bot.conf | 2 ++ src/modules/meta/cog.py | 2 +- src/modules/shop/cog.py | 2 ++ src/wards.py | 18 +++++++++++------- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/config/example-bot.conf b/config/example-bot.conf index a5195d74..343752b0 100644 --- a/config/example-bot.conf +++ b/config/example-bot.conf @@ -11,6 +11,8 @@ ALSO_READ = config/emojis.conf, config/secrets.conf, config/gui.conf asset_path = assets +support_guild = + [ENDPOINTS] guild_log = gem_transaction = diff --git a/src/modules/meta/cog.py b/src/modules/meta/cog.py index e5cc5f4f..b865148d 100644 --- a/src/modules/meta/cog.py +++ b/src/modules/meta/cog.py @@ -32,6 +32,6 @@ class MetaCog(LionCog): ctx.bot, ctx.author, ctx.guild, - show_admin=await low_management(ctx.bot, ctx.author), + show_admin=await low_management(ctx.bot, ctx.author, ctx.guild), ) await ui.run(ctx.interaction) diff --git a/src/modules/shop/cog.py b/src/modules/shop/cog.py index 4892c2f2..a6885751 100644 --- a/src/modules/shop/cog.py +++ b/src/modules/shop/cog.py @@ -117,6 +117,7 @@ class Shopping(LionCog): name=_p('cmd:shop', 'shop'), description=_p('cmd:shop|desc', "Purchase coloures, roles, and other goodies with LionCoins.") ) + @appcmds.guild_only async def shop_group(self, ctx: LionContext): return @@ -124,6 +125,7 @@ class Shopping(LionCog): name=_p('cmd:shop_open', 'open'), description=_p('cmd:shop_open|desc', "Open the server shop.") ) + @appcmds.guild_only async def shop_open_cmd(self, ctx: LionContext): """ Opens the shop UI for the current guild. diff --git a/src/wards.py b/src/wards.py index 6198436b..e66683a0 100644 --- a/src/wards.py +++ b/src/wards.py @@ -21,13 +21,17 @@ async def sys_admin(bot: LionBot, userid: int): return userid in admins -async def high_management(bot: LionBot, member: discord.Member): +async def high_management(bot: LionBot, member: discord.Member, guild=discord.Guild): + if not guild: + return True if await sys_admin(bot, member.id): return True return member.guild_permissions.administrator -async def low_management(bot: LionBot, member: discord.Member): +async def low_management(bot: LionBot, member: discord.Member, guild: discord.Guild): + if not guild: + return True if await high_management(bot, member): return True return member.guild_permissions.manage_guild @@ -42,20 +46,20 @@ async def sys_admin_iward(interaction: discord.Interaction) -> bool: async def high_management_iward(interaction: discord.Interaction) -> bool: if not interaction.guild: return False - return await high_management(interaction.client, interaction.user) + return await high_management(interaction.client, interaction.user, interaction.guild) async def low_management_iward(interaction: discord.Interaction) -> bool: if not interaction.guild: return False - return await low_management(interaction.client, interaction.user) + return await low_management(interaction.client, interaction.user, interaction.guild) # High level ctx wards async def moderator_ctxward(ctx: LionContext) -> bool: if not ctx.guild: return False - passed = await low_management(ctx.bot, ctx.author) + passed = await low_management(ctx.bot, ctx.author, ctx.guild) if passed: return True modrole = ctx.lguild.data.mod_role @@ -85,7 +89,7 @@ async def sys_admin_ward(ctx: LionContext) -> bool: async def high_management_ward(ctx: LionContext) -> bool: if not ctx.guild: return False - passed = await high_management(ctx.bot, ctx.author) + passed = await high_management(ctx.bot, ctx.author, ctx.guild) if passed: return True else: @@ -101,7 +105,7 @@ async def high_management_ward(ctx: LionContext) -> bool: async def low_management_ward(ctx: LionContext) -> bool: if not ctx.guild: return False - passed = await low_management(ctx.bot, ctx.author) + passed = await low_management(ctx.bot, ctx.author, ctx.guild) if passed: return True else: From 60897a3bd929a5e12be9ce5615cf822b55621f29 Mon Sep 17 00:00:00 2001 From: JetRaidz <59957974+JetRaidz@users.noreply.github.com> Date: Tue, 19 Sep 2023 21:53:41 +1200 Subject: [PATCH 06/25] Fix typo in wards Fixed typo in `high_management` function --- src/wards.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wards.py b/src/wards.py index e66683a0..2ac290e1 100644 --- a/src/wards.py +++ b/src/wards.py @@ -21,7 +21,7 @@ async def sys_admin(bot: LionBot, userid: int): return userid in admins -async def high_management(bot: LionBot, member: discord.Member, guild=discord.Guild): +async def high_management(bot: LionBot, member: discord.Member, guild: discord.Guild): if not guild: return True if await sys_admin(bot, member.id): From 17683a7d96fc077ddea650d5c3580435a140e877 Mon Sep 17 00:00:00 2001 From: Conatum Date: Tue, 19 Sep 2023 22:59:01 +0300 Subject: [PATCH 07/25] fix(core): Handle rendering errors. --- src/gui | 2 +- src/meta/LionBot.py | 15 +++++++-- src/meta/LionTree.py | 39 ++++++++++++++++++------ src/modules/pomodoro/cog.py | 3 +- src/modules/pomodoro/timer.py | 23 ++++++++------ src/modules/statistics/ui/leaderboard.py | 12 ++++---- src/utils/ui/leo.py | 36 +++++++++++----------- 7 files changed, 83 insertions(+), 47 deletions(-) diff --git a/src/gui b/src/gui index b781f7f9..ba9ace6c 160000 --- a/src/gui +++ b/src/gui @@ -1 +1 @@ -Subproject commit b781f7f9f21bd094601905eedd0c6463140e818f +Subproject commit ba9ace6ced300123c53287ff1ba4dbd106d1cc46 diff --git a/src/meta/LionBot.py b/src/meta/LionBot.py index 9fb9e5e5..f316c24e 100644 --- a/src/meta/LionBot.py +++ b/src/meta/LionBot.py @@ -12,6 +12,7 @@ from aiohttp import ClientSession from data import Database from utils.lib import tabulate +from gui.errors import RenderingException from .config import Conf from .logger import logging_context, log_context, log_action_stack, log_wrap, set_logging_context @@ -204,13 +205,23 @@ class LionBot(Bot): pass except asyncio.TimeoutError: pass + except RenderingException as e: + logger.info(f"Command failed due to RenderingException: {repr(e)}") + embed = self.tree.rendersplat(e) + try: + await ctx.error_reply(embed=embed) + except discord.HTTPException: + pass except Exception as e: logger.exception( f"Caught an unknown CommandInvokeError while executing: {cmd_str}", extra={'action': 'BotError', 'with_ctx': True} ) - error_embed = discord.Embed(title="Something went wrong!") + error_embed = discord.Embed( + title="Something went wrong!", + colour=discord.Colour.dark_red() + ) error_embed.description = ( "An unexpected error occurred while processing your command!\n" "Our development team has been notified, and the issue will be addressed soon.\n" @@ -246,7 +257,7 @@ class LionBot(Bot): try: await ctx.error_reply(embed=error_embed) - except Exception: + except discord.HTTPException: pass finally: exception.original = HandledException(exception.original) diff --git a/src/meta/LionTree.py b/src/meta/LionTree.py index e5f5624b..4461ef2e 100644 --- a/src/meta/LionTree.py +++ b/src/meta/LionTree.py @@ -8,9 +8,11 @@ from discord.enums import InteractionType from discord.app_commands.namespace import Namespace from utils.lib import tabulate +from gui.errors import RenderingException from .logger import logging_context, set_logging_context, log_wrap, log_action_stack from .errors import SafeCancellation +from .config import conf logger = logging.getLogger(__name__) @@ -29,17 +31,36 @@ class LionTree(CommandTree): except SafeCancellation: # Assume this has already been handled pass + except RenderingException as e: + logger.info(f"Tree interaction failed due to rendering exception: {repr(e)}") + embed = self.rendersplat(e) + await self.error_reply(interaction, embed) except Exception: logger.exception(f"Unhandled exception in interaction: {interaction}", extra={'action': 'TreeError'}) - if not interaction.is_expired(): - splat = self.bugsplat(interaction, error) - try: - if interaction.response.is_done(): - await interaction.followup.send(embed=splat, ephemeral=True) - else: - await interaction.response.send_message(embed=splat, ephemeral=True) - except discord.HTTPException: - pass + embed = self.bugsplat(interaction, error) + await self.error_reply(interaction, embed) + + async def error_reply(self, interaction, embed): + if not interaction.is_expired(): + try: + if interaction.response.is_done(): + await interaction.followup.send(embed=embed, ephemeral=True) + else: + await interaction.response.send_message(embed=embed, ephemeral=True) + except discord.HTTPException: + pass + + def rendersplat(self, e: RenderingException): + embed = discord.Embed( + title="Resource Currently Unavailable!", + description=( + "Sorry, the graphics service is currently unavailable!\n" + "Please try again in a few minutes.\n" + "If the error persists, please contact our [support team]({link})" + ).format(link=conf.bot.support_guild), + colour=discord.Colour.dark_red() + ) + return embed def bugsplat(self, interaction, e): error_embed = discord.Embed(title="Something went wrong!", colour=discord.Colour.red()) diff --git a/src/modules/pomodoro/cog.py b/src/modules/pomodoro/cog.py index 799a3822..f2a5cc41 100644 --- a/src/modules/pomodoro/cog.py +++ b/src/modules/pomodoro/cog.py @@ -386,8 +386,9 @@ class TimerCog(LionCog): ) else: # Display the timer status ephemerally + await ctx.interaction.response.defer(thinking=True, ephemeral=True) status = await timer.current_status(with_notify=False, with_warnings=False) - await ctx.reply(**status.send_args, ephemeral=True) + await ctx.interaction.edit_original_response(**status.edit_args) if error is not None: await ctx.reply(embed=error, ephemeral=True) diff --git a/src/modules/pomodoro/timer.py b/src/modules/pomodoro/timer.py index f169d4f9..9da3b288 100644 --- a/src/modules/pomodoro/timer.py +++ b/src/modules/pomodoro/timer.py @@ -12,6 +12,7 @@ from utils.lib import MessageArgs, utc_now, replace_multiple from core.lion_guild import LionGuild from core.data import CoreData from babel.translator import ctx_locale +from gui.errors import RenderingException from . import babel, logger from .data import TimerData @@ -83,6 +84,7 @@ class Timer: self.destroyed = False def __repr__(self): + # TODO: Add lock status and current state and stage return ( "") - card = await get_timer_card(self.bot, self, stage) - await card.render() - if (ui := self.status_view) is None: ui = self.status_view = TimerStatusUI(self.bot, self, self.channel) await ui.refresh() - return MessageArgs( - content=content, - file=card.as_file(f"pomodoro_{self.data.channelid}.png"), - view=ui - ) + card = await get_timer_card(self.bot, self, stage) + try: + await card.render() + file = card.as_file(f"pomodoro_{self.data.channelid}.png") + args = MessageArgs(content=content, file=file, view=ui) + except RenderingException: + args = MessageArgs(content=content, view=ui) + + return args @log_wrap(action='Send Timer Status') async def send_status(self, delete_last=True, **kwargs): @@ -785,8 +788,8 @@ class Timer: to_next_stage = (current.end - utc_now()).total_seconds() # TODO: Consider request rate and load - if to_next_stage > 1 * 60 - drift: - time_to_sleep = 1 * 60 + if to_next_stage > 5 * 60 - drift: + time_to_sleep = 5 * 60 else: time_to_sleep = to_next_stage diff --git a/src/modules/statistics/ui/leaderboard.py b/src/modules/statistics/ui/leaderboard.py index cc82c2ff..4dadbde8 100644 --- a/src/modules/statistics/ui/leaderboard.py +++ b/src/modules/statistics/ui/leaderboard.py @@ -299,42 +299,42 @@ class LeaderboardUI(StatsUI): @button(label="This Season", style=ButtonStyle.grey) async def season_button(self, press: discord.Interaction, pressed: Button): - await press.response.defer(thinking=True) + await press.response.defer(thinking=True, ephemeral=True) self.current_period = LBPeriod.SEASON self.focused = True await self.refresh(thinking=press) @button(label="Today", style=ButtonStyle.grey) async def day_button(self, press: discord.Interaction, pressed: Button): - await press.response.defer(thinking=True) + await press.response.defer(thinking=True, ephemeral=True) self.current_period = LBPeriod.DAY self.focused = True await self.refresh(thinking=press) @button(label="This Week", style=ButtonStyle.grey) async def week_button(self, press: discord.Interaction, pressed: Button): - await press.response.defer(thinking=True) + await press.response.defer(thinking=True, ephemeral=True) self.current_period = LBPeriod.WEEK self.focused = True await self.refresh(thinking=press) @button(label="This Month", style=ButtonStyle.grey) async def month_button(self, press: discord.Interaction, pressed: Button): - await press.response.defer(thinking=True) + await press.response.defer(thinking=True, ephemeral=True) self.current_period = LBPeriod.MONTH self.focused = True await self.refresh(thinking=press) @button(label="All Time", style=ButtonStyle.grey) async def alltime_button(self, press: discord.Interaction, pressed: Button): - await press.response.defer(thinking=True) + await press.response.defer(thinking=True, ephemeral=True) self.current_period = LBPeriod.ALLTIME self.focused = True await self.refresh(thinking=press) @button(emoji=conf.emojis.backward, style=ButtonStyle.grey) async def prev_button(self, press: discord.Interaction, pressed: Button): - await press.response.defer(thinking=True) + await press.response.defer(thinking=True, ephemeral=True) self.pagen -= 1 self.focused = False await self.refresh(thinking=press) diff --git a/src/utils/ui/leo.py b/src/utils/ui/leo.py index 07979a2f..7459f0f8 100644 --- a/src/utils/ui/leo.py +++ b/src/utils/ui/leo.py @@ -10,6 +10,8 @@ from discord.ui import Modal, View, Item from meta.logger import log_action_stack, logging_context from meta.errors import SafeCancellation +from gui.errors import RenderingException + from . import logger from ..lib import MessageArgs, error_embed @@ -228,6 +230,12 @@ class LeoUI(View): f"Caught a safe cancellation from LeoUI: {e.details}", extra={'action': 'Cancel'} ) + except RenderingException as e: + logger.info( + f"UI interaction failed due to rendering exception: {repr(e)}" + ) + embed = interaction.client.tree.rendersplat(e) + await interaction.client.tree.error_reply(interaction, embed) except Exception: logger.exception( f"Unhandled interaction exception occurred in item {item!r} of LeoUI {self!r} from interaction: " @@ -235,15 +243,8 @@ class LeoUI(View): extra={'with_ctx': True, 'action': 'UIError'} ) # Explicitly handle the bugsplat ourselves - if not interaction.is_expired(): - splat = interaction.client.tree.bugsplat(interaction, error) - try: - if interaction.response.is_done(): - await interaction.followup.send(embed=splat, ephemeral=True) - else: - await interaction.response.send_message(embed=splat, ephemeral=True) - except discord.HTTPException: - pass + splat = interaction.client.tree.bugsplat(interaction, error) + await interaction.client.tree.error_reply(interaction, splat) class MessageUI(LeoUI): @@ -475,21 +476,20 @@ class LeoModal(Modal): """ try: raise error + except RenderingException as e: + logger.info( + f"Modal submit failed due to rendering exception: {repr(e)}" + ) + embed = interaction.client.tree.rendersplat(e) + await interaction.client.tree.error_reply(interaction, embed) except Exception: logger.exception( f"Unhandled interaction exception occurred in {self!r}. Interaction: {interaction.data}", extra={'with_ctx': True, 'action': 'ModalError'} ) # Explicitly handle the bugsplat ourselves - if not interaction.is_expired(): - splat = interaction.client.tree.bugsplat(interaction, error) - try: - if interaction.response.is_done(): - await interaction.followup.send(embed=splat, ephemeral=True) - else: - await interaction.response.send_message(embed=splat, ephemeral=True) - except discord.HTTPException: - pass + splat = interaction.client.tree.bugsplat(interaction, error) + await interaction.client.tree.error_reply(interaction, splat) def error_handler_for(exc): From 519fb976aaf0dc4a7a710ba78dfdce990802ea30 Mon Sep 17 00:00:00 2001 From: Conatum Date: Wed, 20 Sep 2023 20:46:39 +0300 Subject: [PATCH 08/25] feat: Lazy member chunking. No longer try to fetch all members on startup. Instead chunk on-demand. --- requirements.txt | 2 +- src/analytics/snapshot.py | 2 +- src/bot.py | 3 +- src/meta/LionBot.py | 28 ++++++++- src/meta/LionTree.py | 2 +- src/modules/economy/cog.py | 2 +- src/modules/member_admin/cog.py | 23 ++++--- src/modules/member_admin/settings.py | 4 +- src/modules/ranks/cog.py | 10 ++- src/modules/schedule/core/session.py | 13 ++++ src/modules/schedule/core/timeslot.py | 2 +- src/modules/statistics/cog.py | 7 ++- src/modules/statistics/ui/leaderboard.py | 16 ++++- src/modules/sysadmin/guild_log.py | 79 ++++++++++++++---------- src/wards.py | 4 +- 15 files changed, 141 insertions(+), 56 deletions(-) diff --git a/requirements.txt b/requirements.txt index 56555fc6..a34bcc40 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ cachetools==4.2.2 configparser==5.0.2 discord.py iso8601==0.1.16 -psycopg +psycopg[pool] pytz==2021.1 topggpy psutil diff --git a/src/analytics/snapshot.py b/src/analytics/snapshot.py index 565280e1..789439eb 100644 --- a/src/analytics/snapshot.py +++ b/src/analytics/snapshot.py @@ -22,7 +22,7 @@ async def shard_snapshot(): snap = ShardSnapshot( guild_count=len(bot.guilds), voice_count=sum(len(channel.members) for guild in bot.guilds for channel in guild.voice_channels), - member_count=sum(len(guild.members) for guild in bot.guilds), + member_count=sum(guild.member_count for guild in bot.guilds), user_count=len(set(m.id for guild in bot.guilds for m in guild.members)) ) return snap diff --git a/src/bot.py b/src/bot.py index 1e47a9e4..8fda839d 100644 --- a/src/bot.py +++ b/src/bot.py @@ -69,7 +69,8 @@ async def main(): shard_count=sharding.shard_count, help_command=None, proxy=conf.bot.get('proxy', None), - translator=translator + translator=translator, + chunk_guilds_at_startup=False, ) as lionbot: ctx_bot.set(lionbot) try: diff --git a/src/meta/LionBot.py b/src/meta/LionBot.py index f316c24e..cb15a9a2 100644 --- a/src/meta/LionBot.py +++ b/src/meta/LionBot.py @@ -240,7 +240,7 @@ class LionBot(Bot): details['guild'] = f"`{ctx.guild.id}` -- `{ctx.guild.name}`" details['my_guild_perms'] = f"`{ctx.guild.me.guild_permissions.value}`" if ctx.author: - ownerstr = ' (owner)' if ctx.author == ctx.guild.owner else '' + ownerstr = ' (owner)' if ctx.author.id == ctx.guild.owner_id else '' details['author_guild_perms'] = f"`{ctx.author.guild_permissions.value}{ownerstr}`" if ctx.channel.type is discord.enums.ChannelType.private: details['channel'] = "`Direct Message`" @@ -281,3 +281,29 @@ class LionBot(Bot): def add_command(self, command): if not hasattr(command, '_placeholder_group_'): super().add_command(command) + + def request_chunking_for(self, guild): + if not guild.chunked: + return asyncio.create_task( + self._connection.chunk_guild(guild, wait=False, cache=True), + name=f"Background chunkreq for {guild.id}" + ) + + async def on_interaction(self, interaction: discord.Interaction): + """ + Adds the interaction author to guild cache if appropriate. + + This gets run a little bit late, so it is possible the interaction gets handled + without the author being in case. + """ + guild = interaction.guild + user = interaction.user + if guild is not None and user is not None and isinstance(user, discord.Member): + if not guild.get_member(user.id): + guild._add_member(user) + if guild is not None and not guild.chunked: + # Getting an interaction in the guild is a good enough reason to request chunking + logger.info( + f"Unchunked guild requesting chunking after interaction." + ) + self.request_chunking_for(guild) diff --git a/src/meta/LionTree.py b/src/meta/LionTree.py index 4461ef2e..d8275e32 100644 --- a/src/meta/LionTree.py +++ b/src/meta/LionTree.py @@ -82,7 +82,7 @@ class LionTree(CommandTree): details['guild'] = f"`{interaction.guild.id}` -- `{interaction.guild.name}`" details['my_guild_perms'] = f"`{interaction.guild.me.guild_permissions.value}`" if interaction.user: - ownerstr = ' (owner)' if interaction.user == interaction.guild.owner else '' + ownerstr = ' (owner)' if interaction.user.id == interaction.guild.owner_id else '' details['user_guild_perms'] = f"`{interaction.user.guild_permissions.value}{ownerstr}`" if interaction.channel.type is discord.enums.ChannelType.private: details['channel'] = "`Direct Message`" diff --git a/src/modules/economy/cog.py b/src/modules/economy/cog.py index aef2b8b9..9e72e4ce 100644 --- a/src/modules/economy/cog.py +++ b/src/modules/economy/cog.py @@ -190,7 +190,7 @@ class Economy(LionCog): # First fetch the members which currently exist query = self.bot.core.data.Member.table.select_where(guildid=ctx.guild.id) query.select('userid').with_no_adapter() - if 2 * len(targets) < len(ctx.guild.members): + if 2 * len(targets) < ctx.guild.member_count: # More efficient to fetch the targets explicitly query.where(userid=list(targetids)) existent_rows = await query diff --git a/src/modules/member_admin/cog.py b/src/modules/member_admin/cog.py index 6d57f222..6887ed6d 100644 --- a/src/modules/member_admin/cog.py +++ b/src/modules/member_admin/cog.py @@ -181,15 +181,17 @@ class MemberAdminCog(LionCog): finally: self._adding_roles.discard((member.guild.id, member.id)) - @LionCog.listener('on_member_remove') + @LionCog.listener('on_raw_member_remove') @log_wrap(action="Farewell") - async def admin_member_farewell(self, member: discord.Member): + async def admin_member_farewell(self, payload: discord.RawMemberRemoveEvent): # Ignore members that just joined - if (member.guild.id, member.id) in self._adding_roles: + guildid = payload.guild_id + userid = payload.user.id + if (guildid, userid) in self._adding_roles: return # Set lion last_left, creating the lion_member if needed - lion = await self.bot.core.lions.fetch_member(member.guild.id, member.id) + lion = await self.bot.core.lions.fetch_member(guildid, userid) await lion.data.update(last_left=utc_now()) # Save member roles @@ -197,18 +199,21 @@ class MemberAdminCog(LionCog): self.bot.db.conn = conn async with conn.transaction(): await self.data.past_roles.delete_where( - guildid=member.guild.id, - userid=member.id + guildid=guildid, + userid=userid ) # Insert current member roles - if member.roles: + print(type(payload.user)) + if isinstance(payload.user, discord.Member) and payload.user.roles: + member = payload.user await self.data.past_roles.insert_many( ('guildid', 'userid', 'roleid'), - *((member.guild.id, member.id, role.id) for role in member.roles) + *((guildid, userid, role.id) for role in member.roles) ) logger.debug( - f"Stored persisting roles for member in ." + f"Stored persisting roles for member in ." ) + # TODO: Event log, and include info about unchunked members @LionCog.listener('on_guild_join') async def admin_init_guild(self, guild: discord.Guild): diff --git a/src/modules/member_admin/settings.py b/src/modules/member_admin/settings.py index d175c275..deaba142 100644 --- a/src/modules/member_admin/settings.py +++ b/src/modules/member_admin/settings.py @@ -173,7 +173,7 @@ class MemberAdminSettings(SettingGroup): '{guild_name}': guild.name, '{guild_icon}': guild.icon.url if guild.icon else member.default_avatar.url, '{studying_count}': str(active), - '{member_count}': len(guild.members), + '{member_count}': guild.member_count, } recurse_map( @@ -297,7 +297,7 @@ class MemberAdminSettings(SettingGroup): '{guild_name}': guild.name, '{guild_icon}': guild.icon.url if guild.icon else member.default_avatar.url, '{studying_count}': str(active), - '{member_count}': str(len(guild.members)), + '{member_count}': str(guild.member_count), '{last_time}': str(last_seen or member.joined_at.timestamp()), } diff --git a/src/modules/ranks/cog.py b/src/modules/ranks/cog.py index 8e245cf6..5f7e9866 100644 --- a/src/modules/ranks/cog.py +++ b/src/modules/ranks/cog.py @@ -503,7 +503,15 @@ class RankCog(LionCog): # Ensure guild is chunked if not guild.chunked: - members = await guild.chunk() + try: + members = await asyncio.wait_for(guild.chunk(), timeout=60) + except asyncio.TimeoutError: + error = t(_p( + 'rank_refresh|error:cannot_chunk|desc', + "Could not retrieve member list from Discord. Please try again later." + )) + await ui.set_error(error) + return else: members = guild.members ui.stage_members = True diff --git a/src/modules/schedule/core/session.py b/src/modules/schedule/core/session.py index cd884c72..da266a9b 100644 --- a/src/modules/schedule/core/session.py +++ b/src/modules/schedule/core/session.py @@ -253,6 +253,12 @@ class ScheduledSession: overwrites = room.overwrites for member in members: mobj = guild.get_member(member.userid) + if not mobj and not guild.chunked: + self.bot.request_chunking_for(guild) + try: + mobj = await guild.fetch_member(member.userid) + except discord.HTTPException: + mobj = None if mobj: overwrites[mobj] = discord.PermissionOverwrite(connect=True, view_channel=True) try: @@ -297,6 +303,13 @@ class ScheduledSession: } for member in members: mobj = guild.get_member(member.userid) + if not mobj and not guild.chunked: + self.bot.request_chunking_for(guild) + try: + mobj = await guild.fetch_member(member.userid) + except discord.HTTPException: + mobj = None + if mobj: overwrites[mobj] = discord.PermissionOverwrite(connect=True, view_channel=True) try: diff --git a/src/modules/schedule/core/timeslot.py b/src/modules/schedule/core/timeslot.py index 1669b780..200c3e1b 100644 --- a/src/modules/schedule/core/timeslot.py +++ b/src/modules/schedule/core/timeslot.py @@ -440,7 +440,7 @@ class TimeSlot: ) def launch(self) -> asyncio.Task: - self.run_task = asyncio.create_task(self.run()) + self.run_task = asyncio.create_task(self.run(), name=f"TimeSlot {self.slotid}") return self.run_task @log_wrap(action="TimeSlot Run") diff --git a/src/modules/statistics/cog.py b/src/modules/statistics/cog.py index d572f5e2..1ef512d5 100644 --- a/src/modules/statistics/cog.py +++ b/src/modules/statistics/cog.py @@ -1,3 +1,4 @@ +import asyncio import logging from typing import Optional @@ -91,7 +92,11 @@ class StatsCog(LionCog): timestamp=utc_now(), ) await ctx.interaction.response.send_message(embed=waiting_embed) - await ctx.guild.chunk() + try: + await asyncio.wait_for(ctx.guild.chunk(), timeout=10) + pass + except asyncio.TimeoutError: + pass else: await ctx.interaction.response.defer(thinking=True) ui = LeaderboardUI(self.bot, ctx.author, ctx.guild) diff --git a/src/modules/statistics/ui/leaderboard.py b/src/modules/statistics/ui/leaderboard.py index 4dadbde8..bf2f9206 100644 --- a/src/modules/statistics/ui/leaderboard.py +++ b/src/modules/statistics/ui/leaderboard.py @@ -75,6 +75,8 @@ class LeaderboardUI(StatsUI): # (type, period) -> (pagen -> Optional[Future[Card]]) self.cache = {} + self.was_chunked: bool = guild.chunked + async def run(self, interaction: discord.Interaction): self._original = interaction @@ -136,6 +138,7 @@ class LeaderboardUI(StatsUI): # Filter out members which are not in the server and unranked roles and bots # Usually hits cache + self.was_chunked = self.guild.chunked unranked_setting = await self.bot.get_cog('StatsCog').settings.UnrankedRoles.get(self.guild.id) unranked_roleids = set(unranked_setting.data) true_leaderboard = [] @@ -435,12 +438,19 @@ class LeaderboardUI(StatsUI): Generate UI message arguments from stored data """ t = self.bot.translator.t + chunk_warning = t(_p( + 'ui:leaderboard|chunk_warning', + "**Note:** Could not retrieve member list from Discord, so some members may be missing. " + "Try again in a minute!" + )) if self.card is not None: period_start = self.period_starts[self.current_period] header = t(_p( 'ui:leaderboard|since', "Counting statistics since {timestamp}" )).format(timestamp=discord.utils.format_dt(period_start)) + if not self.was_chunked: + header = '\n'.join((header, chunk_warning)) args = MessageArgs( embed=None, content=header, @@ -473,7 +483,11 @@ class LeaderboardUI(StatsUI): )), description=empty_description ) - args = MessageArgs(content=None, embed=embed, files=[]) + args = MessageArgs( + content=chunk_warning if not self.was_chunked else None, + embed=embed, + files=[] + ) return args async def refresh_components(self): diff --git a/src/modules/sysadmin/guild_log.py b/src/modules/sysadmin/guild_log.py index a12d24e2..2e38248a 100644 --- a/src/modules/sysadmin/guild_log.py +++ b/src/modules/sysadmin/guild_log.py @@ -1,3 +1,4 @@ +import asyncio import datetime import discord @@ -22,8 +23,8 @@ class GuildLog(LionCog): embed.set_author(name="Left guild!") # Add more specific information about the guild - embed.add_field(name="Owner", value="{0.name} (ID: {0.id})".format(guild.owner), inline=False) - embed.add_field(name="Members (cached)", value="{}".format(len(guild.members)), inline=False) + embed.add_field(name="Owner", value="<@{}>".format(guild.owner_id), inline=False) + embed.add_field(name="Members", value="{}".format(guild.member_count), inline=False) embed.add_field(name="Now studying in", value="{} guilds".format(len(self.bot.guilds)), inline=False) # Retrieve the guild log channel and log the event @@ -35,39 +36,51 @@ class GuildLog(LionCog): @LionCog.listener('on_guild_join') @log_wrap(action="Log Guild Join") async def log_join_guild(self, guild: discord.Guild): - owner = guild.owner + try: + await asyncio.wait_for(guild.chunk(), timeout=60) + except asyncio.TimeoutError: + pass - bots = 0 - known = 0 - unknown = 0 - other_members = set(mem.id for mem in self.bot.get_all_members() if mem.guild != guild) + # TODO: Add info about when we last joined this guild etc once we have it. - for member in guild.members: - if member.bot: - bots += 1 - elif member.id in other_members: - known += 1 - else: - unknown += 1 + if guild.chunked: + bots = 0 + known = 0 + unknown = 0 + other_members = set(mem.id for mem in self.bot.get_all_members() if mem.guild != guild) + + for member in guild.members: + if member.bot: + bots += 1 + elif member.id in other_members: + known += 1 + else: + unknown += 1 + + mem1 = "people I know" if known != 1 else "person I know" + mem2 = "new friends" if unknown != 1 else "new friend" + mem3 = "bots" if bots != 1 else "bot" + mem4 = "total members" + known = "`{}`".format(known) + unknown = "`{}`".format(unknown) + bots = "`{}`".format(bots) + total = "`{}`".format(guild.member_count) + mem_str = "{0:<5}\t{4},\n{1:<5}\t{5},\n{2:<5}\t{6}, and\n{3:<5}\t{7}.".format( + known, + unknown, + bots, + total, + mem1, + mem2, + mem3, + mem4 + ) + else: + mem_str = ( + "`{count}` total members.\n" + "(Could not chunk guild within `60` seconds.)" + ).format(count=guild.member_count) - mem1 = "people I know" if known != 1 else "person I know" - mem2 = "new friends" if unknown != 1 else "new friend" - mem3 = "bots" if bots != 1 else "bot" - mem4 = "total members" - known = "`{}`".format(known) - unknown = "`{}`".format(unknown) - bots = "`{}`".format(bots) - total = "`{}`".format(guild.member_count) - mem_str = "{0:<5}\t{4},\n{1:<5}\t{5},\n{2:<5}\t{6}, and\n{3:<5}\t{7}.".format( - known, - unknown, - bots, - total, - mem1, - mem2, - mem3, - mem4 - ) created = "".format(int(guild.created_at.timestamp())) embed = discord.Embed( @@ -77,7 +90,7 @@ class GuildLog(LionCog): ) embed.set_author(name="Joined guild!") - embed.add_field(name="Owner", value="{0} (ID: {0.id})".format(owner), inline=False) + embed.add_field(name="Owner", value="<@{}>".format(guild.owner_id), inline=False) embed.add_field(name="Created at", value=created, inline=False) embed.add_field(name="Members", value=mem_str, inline=False) embed.add_field(name="Now studying in", value="{} guilds".format(len(self.bot.guilds)), inline=False) diff --git a/src/wards.py b/src/wards.py index 2ac290e1..5db46eb9 100644 --- a/src/wards.py +++ b/src/wards.py @@ -32,7 +32,7 @@ async def high_management(bot: LionBot, member: discord.Member, guild: discord.G async def low_management(bot: LionBot, member: discord.Member, guild: discord.Guild): if not guild: return True - if await high_management(bot, member): + if await high_management(bot, member, guild): return True return member.guild_permissions.manage_guild @@ -196,7 +196,7 @@ async def equippable_role(bot: LionBot, target_role: discord.Role, actor: discor "You need the `MANAGE_ROLES` permission before you can configure roles!" )).format(role=target_role.mention) ) - elif actor.top_role <= target_role and not actor == guild.owner: + elif actor.top_role <= target_role and not actor.id == guild.owner_id: raise UserInputError( t(_p( 'ward:equippable_role|error:actor_top_role', From dd42c82b632275f58dc42115d1527a9779873ebb Mon Sep 17 00:00:00 2001 From: Conatum Date: Wed, 20 Sep 2023 23:06:36 +0300 Subject: [PATCH 09/25] fix(babel): Improve locale logging. --- src/babel/cog.py | 9 +++--- src/babel/enums.py | 71 ++++++++++++++++++++++------------------- src/babel/settings.py | 6 ++-- src/babel/translator.py | 36 +++++++++++++-------- 4 files changed, 70 insertions(+), 52 deletions(-) diff --git a/src/babel/cog.py b/src/babel/cog.py index bd535a3e..8f18ddf9 100644 --- a/src/babel/cog.py +++ b/src/babel/cog.py @@ -230,14 +230,15 @@ class BabelCog(LionCog): supported = self.bot.translator.supported_locales formatted = [] for locale in supported: - name = locale_names.get(locale.replace('_', '-'), None) - if name: - localestr = f"{locale} ({t(name)})" + names = locale_names.get(locale.replace('_', '-'), None) + if names: + local_name, native_name = names + localestr = f"{native_name} ({t(local_name)})" else: localestr = locale formatted.append((locale, localestr)) - matching = {item for item in formatted if partial in item[1]} + matching = {item for item in formatted if partial in item[1] or partial in item[0]} if matching: choices = [ appcmds.Choice(name=localestr, value=locale) diff --git a/src/babel/enums.py b/src/babel/enums.py index 6ab77a74..9e73da33 100644 --- a/src/babel/enums.py +++ b/src/babel/enums.py @@ -38,37 +38,44 @@ class LocaleMap(Enum): hebrew = 'he-IL' +# Original Discord names locale_names = { - 'en-US': _p('localenames|locale:en-US', "American English"), - 'en-GB': _p('localenames|locale:en-GB', "British English"), - 'bg': _p('localenames|locale:bg', "Bulgarian"), - 'zh-CN': _p('localenames|locale:zh-CN', "Chinese"), - 'zh-TW': _p('localenames|locale:zh-TW', "Taiwan Chinese"), - 'hr': _p('localenames|locale:hr', "Croatian"), - 'cs': _p('localenames|locale:cs', "Czech"), - 'da': _p('localenames|locale:da', "Danish"), - 'nl': _p('localenames|locale:nl', "Dutch"), - 'fi': _p('localenames|locale:fi', "Finnish"), - 'fr': _p('localenames|locale:fr', "French"), - 'de': _p('localenames|locale:de', "German"), - 'el': _p('localenames|locale:el', "Greek"), - 'hi': _p('localenames|locale:hi', "Hindi"), - 'hu': _p('localenames|locale:hu', "Hungarian"), - 'it': _p('localenames|locale:it', "Italian"), - 'ja': _p('localenames|locale:ja', "Japanese"), - 'ko': _p('localenames|locale:ko', "Korean"), - 'lt': _p('localenames|locale:lt', "Lithuanian"), - 'no': _p('localenames|locale:no', "Norwegian"), - 'pl': _p('localenames|locale:pl', "Polish"), - 'pt-BR': _p('localenames|locale:pt-BR', "Brazil Portuguese"), - 'ro': _p('localenames|locale:ro', "Romanian"), - 'ru': _p('localenames|locale:ru', "Russian"), - 'es-ES': _p('localenames|locale:es-ES', "Spain Spanish"), - 'sv-SE': _p('localenames|locale:sv-SE', "Swedish"), - 'th': _p('localenames|locale:th', "Thai"), - 'tr': _p('localenames|locale:tr', "Turkish"), - 'uk': _p('localenames|locale:uk', "Ukrainian"), - 'vi': _p('localenames|locale:vi', "Vietnamese"), - 'he': _p('localenames|locale:he', "Hebrew"), - 'he-IL': _p('localenames|locale:he_IL', "Hebrew (Israel)"), + 'id': (_p('localenames|locale:id', "Indonesian"), "Bahasa Indonesia"), + 'da': (_p('localenames|locale:da', "Danish"), "Dansk"), + 'de': (_p('localenames|locale:de', "German"), "Deutsch"), + 'en-GB': (_p('localenames|locale:en-GB', "English, UK"), "English, UK"), + 'en-US': (_p('localenames|locale:en-US', "English, US"), "English, US"), + 'es-ES': (_p('localenames|locale:es-ES', "Spanish"), "Español"), + 'fr': (_p('localenames|locale:fr', "French"), "Français"), + 'hr': (_p('localenames|locale:hr', "Croatian"), "Hrvatski"), + 'it': (_p('localenames|locale:it', "Italian"), "Italiano"), + 'lt': (_p('localenames|locale:lt', "Lithuanian"), "Lietuviškai"), + 'hu': (_p('localenames|locale:hu', "Hungarian"), "Magyar"), + 'nl': (_p('localenames|locale:nl', "Dutch"), "Nederlands"), + 'no': (_p('localenames|locale:no', "Norwegian"), "Norsk"), + 'pl': (_p('localenames|locale:pl', "Polish"), "Polski"), + 'pt-BR': (_p('localenames|locale:pt-BR', "Portuguese, Brazilian"), "Português do Brasil"), + 'ro': (_p('localenames|locale:ro', "Romanian, Romania"), "Română"), + 'fi': (_p('localenames|locale:fi', "Finnish"), "Suomi"), + 'sv-SE': (_p('localenames|locale:sv-SE', "Swedish"), "Svenska"), + 'vi': (_p('localenames|locale:vi', "Vietnamese"), "Tiếng Việt"), + 'tr': (_p('localenames|locale:tr', "Turkish"), "Türkçe"), + 'cs': (_p('localenames|locale:cs', "Czech"), "Čeština"), + 'el': (_p('localenames|locale:el', "Greek"), "Ελληνικά"), + 'bg': (_p('localenames|locale:bg', "Bulgarian"), "български"), + 'ru': (_p('localenames|locale:ru', "Russian"), "Pусский"), + 'uk': (_p('localenames|locale:uk', "Ukrainian"), "Українська"), + 'hi': (_p('localenames|locale:hi', "Hindi"), "हिन्दी"), + 'th': (_p('localenames|locale:th', "Thai"), "ไทย"), + 'zh-CN': (_p('localenames|locale:zh-CN', "Chinese, China"), "中文"), + 'ja': (_p('localenames|locale:ja', "Japanese"), "日本語"), + 'zh-TW': (_p('localenames|locale:zh-TW', "Chinese, Taiwan"), "繁體中文"), + 'ko': (_p('localenames|locale:ko', "Korean"), "한국어"), +} + +# More names for languages not supported by Discord +locale_names |= { + 'he': (_p('localenames|locale:he', "Hebrew"), "Hebrew"), + 'he-IL': (_p('localenames|locale:he-IL', "Hebrew"), "Hebrew"), + 'ceaser': (_p('localenames|locale:test', "Test Language"), "dfbtfs"), } diff --git a/src/babel/settings.py b/src/babel/settings.py index bacfda87..47da6afe 100644 --- a/src/babel/settings.py +++ b/src/babel/settings.py @@ -43,9 +43,9 @@ class LocaleSetting(StringSetting): if data is None: formatted = t(_p('settype:locale|formatted:unset', "Unset")) else: - name = locale_names.get(data, None) - if name: - formatted = f"`{data} ({t(name)})`" + if data in locale_names: + local_name, native_name = locale_names[data] + formatted = f"`{native_name} ({t(local_name)})`" else: formatted = f"`{data}`" return formatted diff --git a/src/babel/translator.py b/src/babel/translator.py index 0d4141b3..6e5fe904 100644 --- a/src/babel/translator.py +++ b/src/babel/translator.py @@ -47,33 +47,43 @@ class LeoBabel(Translator): Initialise the gettext translators for the supported_locales. """ self.read_supported() + missing = [] + loaded = [] for locale in self.supported_locales: for domain in self.supported_domains: if locale == SOURCE_LOCALE: continue try: translator = gettext.translation(domain, "locales/", languages=[locale]) + loaded.append(f"Loaded translator for ") except OSError: # Presume translation does not exist - logger.warning(f"Could not load translator for supported ") - pass - else: - logger.debug(f"Loaded translator for ") - self.translators[locale][domain] = translator + missing.append(f"Could not load translator for supported ") + translator = null + + self.translators[locale][domain] = translator + if missing: + logger.warning('\n'.join(("Missing Translators:", *missing))) + if loaded: + logger.debug('\n'.join(("Loaded Translators:", *loaded))) async def unload(self): self.translators.clear() def get_translator(self, locale, domain): if locale == SOURCE_LOCALE: - return null - - translator = self.translators[locale].get(domain, None) - if translator is None: - logger.warning( - f"Translator missing for requested and . Setting NullTranslator." - ) - self.translators[locale][domain] = null + translator = null + elif locale in self.supported_locales and domain in self.supported_domains: + translator = self.translators[locale].get(domain, None) + if translator is None: + # This should never really happen because we already loaded the supported translators + logger.warning( + f"Translator missing for supported " + "and . Setting NullTranslator." + ) + translator = self.translators[locale][domain] = null + else: + # Unsupported translator = null return translator From 4f39d873deb0c23c715fed059c2b3051fe04ae6b Mon Sep 17 00:00:00 2001 From: Conatum Date: Wed, 20 Sep 2023 23:44:54 +0300 Subject: [PATCH 10/25] fix(ranks): Fix refreshui monitor. --- src/modules/ranks/cog.py | 1 + src/modules/ranks/ui/refresh.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/modules/ranks/cog.py b/src/modules/ranks/cog.py index 5f7e9866..1af23900 100644 --- a/src/modules/ranks/cog.py +++ b/src/modules/ranks/cog.py @@ -495,6 +495,7 @@ class RankCog(LionCog): await interaction.response.defer(thinking=False) ui = RankRefreshUI(self.bot, guild, callerid=interaction.user.id, timeout=None) await ui.send(interaction.channel) + ui.start() # Retrieve fresh rank roles ranks = await self.get_guild_ranks(guild.id, refresh=True) diff --git a/src/modules/ranks/ui/refresh.py b/src/modules/ranks/ui/refresh.py index e01dc059..b45b3b6e 100644 --- a/src/modules/ranks/ui/refresh.py +++ b/src/modules/ranks/ui/refresh.py @@ -64,9 +64,12 @@ class RankRefreshUI(MessageUI): def poke(self): self._wakeup.set() + def start(self): + self._loop_task = asyncio.create_task(self._refresh_loop(), name='Rank RefreshUI Monitor') + async def run(self, *args, **kwargs): await super().run(*args, **kwargs) - self._loop_task = asyncio.create_task(self._refresh_loop(), name='refresh ui loop') + self.start() async def cleanup(self): if self._loop_task and not self._loop_task.done(): From a96f320e81797a6d14389254f1e72352aefd3a0f Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 21 Sep 2023 01:22:32 +0300 Subject: [PATCH 11/25] fix(ranks): Flush rank cache from preview UI. --- src/modules/ranks/ui/preview.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/modules/ranks/ui/preview.py b/src/modules/ranks/ui/preview.py index 2f437680..21e1064f 100644 --- a/src/modules/ranks/ui/preview.py +++ b/src/modules/ranks/ui/preview.py @@ -112,6 +112,7 @@ class RankPreviewUI(MessageUI): await submit.response.defer(thinking=False) if self.parent is not None: asyncio.create_task(self.parent.refresh()) + self.bot.get_cog('RankCog').flush_guild_ranks(self.guild.id) await self.refresh() @button(label="DELETE_PLACEHOLDER", style=ButtonStyle.red) @@ -130,6 +131,7 @@ class RankPreviewUI(MessageUI): role = None await self.rank.delete() + self.bot.get_cog('RankCog').flush_guild_ranks(self.guild.id) mention = role.mention if role else str(self.rank.roleid) @@ -231,6 +233,7 @@ class RankPreviewUI(MessageUI): elif role.is_assignable(): # Update the rank role await self.rank.update(roleid=role.id) + self.bot.get_cog('RankCog').flush_guild_ranks(self.guild.id) if self.parent is not None and not self.parent.is_finished(): asyncio.create_task(self.parent.refresh()) await self.refresh(thinking=selection) From 4f809396a64ecba268af5040c61c95722a0f3559 Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 21 Sep 2023 01:37:24 +0300 Subject: [PATCH 12/25] fix(ranks): Account for deleted role. --- src/modules/ranks/cog.py | 2 +- src/modules/statistics/graphics/profile.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/ranks/cog.py b/src/modules/ranks/cog.py index 1af23900..47d256a8 100644 --- a/src/modules/ranks/cog.py +++ b/src/modules/ranks/cog.py @@ -438,7 +438,7 @@ class RankCog(LionCog): required = format_stat_range(rank_type, rank.required, short=False) key_map = { - '{role_name}': role.name, + '{role_name}': role.name if role else 'Unknown', '{guild_name}': guild.name, '{user_name}': member.name, '{role_id}': role.id, diff --git a/src/modules/statistics/graphics/profile.py b/src/modules/statistics/graphics/profile.py index a642793f..3a413d4a 100644 --- a/src/modules/statistics/graphics/profile.py +++ b/src/modules/statistics/graphics/profile.py @@ -44,7 +44,7 @@ async def get_profile_card(bot: LionBot, userid: int, guildid: int): if crank: roleid = crank.roleid role = guild.get_role(roleid) - name = role.name if role else str(role.id) + name = role.name if role else 'Unknown Rank' minimum = crank.required maximum = nrank.required if nrank else None rangestr = format_stat_range(rank_type, minimum, maximum) @@ -63,7 +63,7 @@ async def get_profile_card(bot: LionBot, userid: int, guildid: int): if nrank: roleid = nrank.roleid role = guild.get_role(roleid) - name = role.name if role else str(role.id) + name = role.name if role else 'Unknown Rank' minimum = nrank.required guild_ranks = await ranks.get_guild_ranks(guildid) From 2cf998f578d210a4195eac2ee943daead418b9f0 Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 21 Sep 2023 02:48:48 +0300 Subject: [PATCH 13/25] fix(ranks): More thorough role scan. --- src/modules/ranks/cog.py | 155 ++++++++++++++++++++++++++++++--------- 1 file changed, 122 insertions(+), 33 deletions(-) diff --git a/src/modules/ranks/cog.py b/src/modules/ranks/cog.py index 47d256a8..f7878c15 100644 --- a/src/modules/ranks/cog.py +++ b/src/modules/ranks/cog.py @@ -286,27 +286,78 @@ class RankCog(LionCog): await task async def _role_check(self, session_rank: SeasonRank): - guild = self.bot.get_guild(session_rank.guildid) - member = guild.get_member(session_rank.userid) - crank = session_rank.current_rank - roleid = crank.roleid if crank else None - last_roleid = session_rank.rankrow.last_roleid - if guild is not None and member is not None and roleid != last_roleid: - new_role = guild.get_role(roleid) if roleid else None - last_role = guild.get_role(last_roleid) if last_roleid else None + """ + Update the member's rank roles, if required. + """ + guildid = session_rank.guildid + guild = self.bot.get_guild(guildid) + + userid = session_rank.userid + member = guild.get_member(userid) + + if guild is not None and member is not None and guild.me.guild_permissions.manage_roles: + ranks = await self.get_guild_ranks(guildid) + + crank = session_rank.current_rank + current_roleid = crank.roleid if crank else None + + # First gather rank roleids, note that the last_roleid is an 'honourary' roleid + last_roleid = session_rank.rankrow.last_roleid + rank_roleids = {rank.roleid for rank in ranks} + rank_roleids.add(last_roleid) + + # Gather member roleids + mem_roleids = {role.id: role for role in member.roles} + + # Calculate diffs + to_add = guild.get_role(current_roleid) if (current_roleid not in mem_roleids) else None + to_rm = [ + role for roleid, role in mem_roleids.items() + if roleid in rank_roleids and roleid != current_roleid + ] + + # Now update roles new_last_roleid = last_roleid - if guild.me.guild_permissions.manage_roles: + + # TODO: Event log here, including errors + to_rm = [role for role in to_rm if role.is_assignable()] + if to_rm: try: - if last_role and last_role.is_assignable(): - await member.remove_roles(last_role) - new_last_roleid = None - if new_role and new_role.is_assignable(): - await member.add_roles(new_role) - new_last_roleid = roleid - except discord.HTTPClient: - pass - if new_last_roleid != last_roleid: - await session_rank.rankrow.update(last_roleid=new_last_roleid) + await member.remove_roles( + *to_rm, + reason="Removing Old Rank Roles", + atomic=True + ) + roleids = ', '.join(str(role.id) for role in to_rm) + logger.info( + f"Removed old rank roles from in : {roleids}" + ) + new_last_roleid = None + except discord.HTTPException: + logger.warning( + f"Unexpected error removing old rank roles from in : {to_rm}", + exc_info=True + ) + + if to_add and to_add.is_assignable(): + try: + await member.add_roles( + to_add, + reason="Rewarding Activity Rank", + atomic=True + ) + logger.info( + f"Rewarded rank role to in ." + ) + new_last_roleid = to_add.id + except discord.HTTPException: + logger.warning( + f"Unexpected error giving in their rank role ", + exc_info=True + ) + + if new_last_roleid != last_roleid: + await session_rank.rankrow.update(last_roleid=new_last_roleid) @log_wrap(action="Update Rank") async def update_rank(self, session_rank): @@ -336,23 +387,61 @@ class RankCog(LionCog): if member is None: return - new_role = guild.get_role(new_rank.roleid) - if last_roleid := session_rank.rankrow.last_roleid: - last_role = guild.get_role(last_roleid) - else: - last_role = None + last_roleid = session_rank.rankrow.last_roleid + # Update ranks if guild.me.guild_permissions.manage_roles: - try: - if last_role and last_role.is_assignable(): - await member.remove_roles(last_role) + # First gather rank roleids, note that the last_roleid is an 'honourary' roleid + rank_roleids = {rank.roleid for rank in ranks} + rank_roleids.add(last_roleid) + + # Gather member roleids + mem_roleids = {role.id: role for role in member.roles} + + # Calculate diffs + to_add = guild.get_role(new_rank.roleid) if (new_rank.roleid not in mem_roleids) else None + to_rm = [ + role for roleid, role in mem_roleids.items() + if roleid in rank_roleids and roleid != new_rank.roleid + ] + + # Now update roles + # TODO: Event log here, including errors + to_rm = [role for role in to_rm if role.is_assignable()] + if to_rm: + try: + await member.remove_roles( + *to_rm, + reason="Removing Old Rank Roles", + atomic=True + ) + roleids = ', '.join(str(role.id) for role in to_rm) + logger.info( + f"Removed old rank roles from in : {roleids}" + ) last_roleid = None - if new_role and new_role.is_assignable(): - await member.add_roles(new_role) - last_roleid = new_role.id - except discord.HTTPException: - # TODO: Event log either way - pass + except discord.HTTPException: + logger.warning( + f"Unexpected error removing old rank roles from in : {to_rm}", + exc_info=True + ) + + if to_add and to_add.is_assignable(): + try: + await member.add_roles( + to_add, + reason="Rewarding Activity Rank", + atomic=True + ) + logger.info( + f"Rewarded rank role to in ." + ) + last_roleid=to_add.id + except discord.HTTPException: + logger.warning( + f"Unexpected error giving in their rank role ", + exc_info=True + ) # Update MemberRank row column = { From 61de28a9094d699ad6f9fe6366ca264044d66337 Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 21 Sep 2023 09:38:46 +0300 Subject: [PATCH 14/25] fix(ranks): Fix creation permission check. --- src/modules/ranks/ui/overview.py | 25 +++++++------------------ src/modules/ranks/ui/preview.py | 22 +++++----------------- 2 files changed, 12 insertions(+), 35 deletions(-) diff --git a/src/modules/ranks/ui/overview.py b/src/modules/ranks/ui/overview.py index 3562bfba..27cb77d1 100644 --- a/src/modules/ranks/ui/overview.py +++ b/src/modules/ranks/ui/overview.py @@ -12,6 +12,7 @@ from data import ORDER from utils.ui import MessageUI, Confirm from utils.lib import MessageArgs +from wards import equippable_role from babel.translator import ctx_translator from .. import babel, logger @@ -185,25 +186,11 @@ class RankOverviewUI(MessageUI): or edit an existing rank, or throw an error if the role is @everyone or not manageable by the client. """ + role: discord.Role = selected.values[0] - if role >= selection.user.top_role: - # Do not allow user to manage a role above their own top role - t = self.bot.translator.t - error = t(_p( - 'ui:rank_overview|menu:roles|error:above_caller', - "You have insufficient permissions to assign {mention} as a rank role! " - "You may only manage roles below your top role." - )).format(mention=role.mention) - embed = discord.Embed( - title=t(_p( - 'ui:rank_overview|menu:roles|error:above_caller|title', - "Insufficient permissions!" - )), - description=error, - colour=discord.Colour.brand_red() - ) - await selection.response.send_message(embed=embed, ephemeral=True) - elif role.is_assignable(): + + if role.is_assignable(): + # Create or edit the selected role existing = next((rank for rank in self.ranks if rank.roleid == role.id), None) if existing: # Display and edit the given role @@ -216,6 +203,8 @@ class RankOverviewUI(MessageUI): ) else: # Create new rank based on role + # Need to check the calling author has authority to manage this role + await equippable_role(self.bot, role, selection.user) await RankEditor.create_rank( selection, self.rank_type, diff --git a/src/modules/ranks/ui/preview.py b/src/modules/ranks/ui/preview.py index 21e1064f..7dcca0e4 100644 --- a/src/modules/ranks/ui/preview.py +++ b/src/modules/ranks/ui/preview.py @@ -7,6 +7,7 @@ from discord.ui.button import button, Button, ButtonStyle from meta import conf, LionBot from core.data import RankType +from wards import equippable_role from utils.ui import MessageUI, AButton, AsComponents from utils.lib import MessageArgs, replace_multiple @@ -214,24 +215,11 @@ class RankPreviewUI(MessageUI): role: discord.Role = selected.values[0] await selection.response.defer(thinking=True, ephemeral=True) - if role >= selection.user.top_role: - # Do not allow user to manage a role above their own top role - error = t(_p( - 'ui:rank_preview|menu:roles|error:above_caller', - "You have insufficient permissions to assign {mention} as a rank role! " - "You may only manage roles below your top role." - )) - embed = discord.Embed( - title=t(_p( - 'ui:rank_preview|menu:roles|error:above_caller|title', - "Insufficient permissions!" - )), - description=error, - colour=discord.Colour.brand_red() - ) - await selection.response.send_message(embed=embed, ephemeral=True) - elif role.is_assignable(): + if role.is_assignable(): # Update the rank role + # Generic permission check for the new role + await equippable_role(self.bot, role, selection.user) + await self.rank.update(roleid=role.id) self.bot.get_cog('RankCog').flush_guild_ranks(self.guild.id) if self.parent is not None and not self.parent.is_finished(): From ff0bb17d29ef4118514b74f9ee69c8e57093f959 Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 21 Sep 2023 10:14:46 +0300 Subject: [PATCH 15/25] feat(logging): Add locale to log info. --- src/meta/LionBot.py | 2 ++ src/meta/LionContext.py | 2 ++ src/meta/LionTree.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/meta/LionBot.py b/src/meta/LionBot.py index cb15a9a2..4e842859 100644 --- a/src/meta/LionBot.py +++ b/src/meta/LionBot.py @@ -13,6 +13,7 @@ from aiohttp import ClientSession from data import Database from utils.lib import tabulate from gui.errors import RenderingException +from babel.translator import ctx_locale from .config import Conf from .logger import logging_context, log_context, log_action_stack, log_wrap, set_logging_context @@ -236,6 +237,7 @@ class LionBot(Bot): details['cmd'] = f"`{ctx.command.qualified_name}`" if ctx.author: details['author'] = f"`{ctx.author.id}` -- `{ctx.author}`" + details['locale'] = f"`{ctx_locale.get()}`" if ctx.guild: details['guild'] = f"`{ctx.guild.id}` -- `{ctx.guild.name}`" details['my_guild_perms'] = f"`{ctx.guild.me.guild_permissions.value}`" diff --git a/src/meta/LionContext.py b/src/meta/LionContext.py index cef6754f..4aab2b19 100644 --- a/src/meta/LionContext.py +++ b/src/meta/LionContext.py @@ -6,6 +6,7 @@ from typing import Optional, TYPE_CHECKING import discord from discord.enums import ChannelType from discord.ext.commands import Context +from babel.translator import ctx_locale if TYPE_CHECKING: from .LionBot import LionBot @@ -79,6 +80,7 @@ class LionContext(Context['LionBot']): parts['alias'] = f"\"{self.invoked_with}\"" if self.command_failed: parts['failed'] = self.command_failed + parts['locale'] = f"\"{ctx_locale.get()}\"" return "".format( ' '.join(f"{name}={value}" for name, value in parts.items()) diff --git a/src/meta/LionTree.py b/src/meta/LionTree.py index d8275e32..5969a01e 100644 --- a/src/meta/LionTree.py +++ b/src/meta/LionTree.py @@ -9,6 +9,7 @@ from discord.app_commands.namespace import Namespace from utils.lib import tabulate from gui.errors import RenderingException +from babel.translator import ctx_locale from .logger import logging_context, set_logging_context, log_wrap, log_action_stack from .errors import SafeCancellation @@ -76,6 +77,7 @@ class LionTree(CommandTree): details['interactiontype'] = f"`{interaction.type}`" if interaction.command: details['cmd'] = f"`{interaction.command.qualified_name}`" + details['locale'] = f"`{ctx_locale.get()}`" if interaction.user: details['user'] = f"`{interaction.user.id}` -- `{interaction.user}`" if interaction.guild: From 970e652fdcc61dd62530cc687a287758ec6f77a8 Mon Sep 17 00:00:00 2001 From: Conatum Date: Fri, 22 Sep 2023 08:38:59 +0300 Subject: [PATCH 16/25] feat(meta): Add a system status reporter. --- src/bot.py | 23 +++++ src/meta/LionBot.py | 32 +++++++ src/meta/monitor.py | 139 +++++++++++++++++++++++++++++++ src/modules/pomodoro/cog.py | 14 ++++ src/modules/schedule/cog.py | 49 +++++++++++ src/modules/sysadmin/exec_cog.py | 18 +++- 6 files changed, 272 insertions(+), 3 deletions(-) create mode 100644 src/meta/monitor.py diff --git a/src/bot.py b/src/bot.py index 8fda839d..49dceb11 100644 --- a/src/bot.py +++ b/src/bot.py @@ -9,6 +9,7 @@ from meta import LionBot, conf, sharding, appname, shard_talk from meta.app import shardname from meta.logger import log_context, log_action_stack, setup_main_logger from meta.context import ctx_bot +from meta.monitor import ComponentMonitor, StatusLevel, ComponentStatus from data import Database @@ -29,6 +30,25 @@ logger = logging.getLogger(__name__) db = Database(conf.data['args']) +async def _data_monitor() -> ComponentStatus: + """ + Component monitor callback for the database. + """ + data = { + 'stats': str(db.pool.get_stats()) + } + if not db.pool._opened: + level = StatusLevel.WAITING + info = "(WAITING) Database Pool is not opened." + elif db.pool._closed: + level = StatusLevel.ERRORED + info = "(ERROR) Database Pool is closed." + else: + level = StatusLevel.OKAY + info = "(OK) Database Pool statistics: {stats}" + return ComponentStatus(level, info, info, data) + + async def main(): log_action_stack.set(("Initialising",)) logger.info("Initialising StudyLion") @@ -73,6 +93,9 @@ async def main(): chunk_guilds_at_startup=False, ) as lionbot: ctx_bot.set(lionbot) + lionbot.system_monitor.add_component( + ComponentMonitor('Database', _data_monitor) + ) try: log_context.set(f"APP: {appname}") logger.info("StudyLion initialised, starting!", extra={'action': 'Starting'}) diff --git a/src/meta/LionBot.py b/src/meta/LionBot.py index 4e842859..ca699ffb 100644 --- a/src/meta/LionBot.py +++ b/src/meta/LionBot.py @@ -21,6 +21,7 @@ from .context import context from .LionContext import LionContext from .LionTree import LionTree from .errors import HandledException, SafeCancellation +from .monitor import SystemMonitor, ComponentMonitor, StatusLevel, ComponentStatus if TYPE_CHECKING: from core import CoreCog @@ -48,9 +49,40 @@ class LionBot(Bot): self.core: Optional['CoreCog'] = None self.translator = translator + self.system_monitor = SystemMonitor() + self.monitor = ComponentMonitor('LionBot', self._monitor_status) + self.system_monitor.add_component(self.monitor) + self._locks = WeakValueDictionary() self._running_events = set() + async def _monitor_status(self): + if self.is_closed(): + level = StatusLevel.ERRORED + info = "(ERROR) Websocket is closed" + data = {} + elif self.is_ws_ratelimited(): + level = StatusLevel.WAITING + info = "(WAITING) Websocket is ratelimited" + data = {} + elif not self.is_ready(): + level = StatusLevel.STARTING + info = "(STARTING) Not yet ready" + data = {} + else: + level = StatusLevel.OKAY + info = ( + "(OK) " + "Logged in with {guild_count} guilds, " + ", websocket latency {latency}, and {events} running events." + ) + data = { + 'guild_count': len(self.guilds), + 'latency': self.latency, + 'events': len(self._running_events), + } + return ComponentStatus(level, info, info, data) + async def setup_hook(self) -> None: log_context.set(f"APP: {self.application_id}") await self.app_ipc.connect() diff --git a/src/meta/monitor.py b/src/meta/monitor.py new file mode 100644 index 00000000..474c51f4 --- /dev/null +++ b/src/meta/monitor.py @@ -0,0 +1,139 @@ +import logging +import asyncio +from enum import IntEnum +from collections import deque, ChainMap +import datetime as dt + +logger = logging.getLogger(__name__) + + +class StatusLevel(IntEnum): + ERRORED = -2 + UNSURE = -1 + WAITING = 0 + STARTING = 1 + OKAY = 2 + + @property + def symbol(self): + return symbols[self] + + +symbols = { + StatusLevel.ERRORED: '🟥', + StatusLevel.UNSURE: '🟧', + StatusLevel.WAITING: '⬜', + StatusLevel.STARTING: '🟫', + StatusLevel.OKAY: '🟩', +} + + +class ComponentStatus: + def __init__(self, level: StatusLevel, short_formatstr: str, long_formatstr: str, data: dict = {}): + self.level = level + self.short_formatstr = short_formatstr + self.long_formatstr = long_formatstr + self.data = data + self.created_at = dt.datetime.now(tz=dt.timezone.utc) + + def format_args(self): + extra = { + 'created_at': self.created_at, + 'level': self.level, + 'symbol': self.level.symbol, + } + return ChainMap(extra, self.data) + + @property + def short(self): + return self.short_formatstr.format(**self.format_args()) + + @property + def long(self): + return self.long_formatstr.format(**self.format_args()) + + +class ComponentMonitor: + _name = None + + def __init__(self, name=None, callback=None): + self._callback = callback + self.name = name or self._name + if not self.name: + raise ValueError("ComponentMonitor must have a name") + + async def _make_status(self, *args, **kwargs): + if self._callback is not None: + return await self._callback(*args, **kwargs) + else: + raise NotImplementedError + + async def status(self) -> ComponentStatus: + try: + status = await self._make_status() + except Exception as e: + logger.exception( + f"Status callback for component '{self.name}' failed. This should not happen." + ) + status = ComponentStatus( + level=StatusLevel.UNSURE, + short_formatstr="Status callback for '{name}' failed with error '{error}'", + long_formatstr="Status callback for '{name}' failed with error '{error}'", + data={ + 'name': self.name, + 'error': repr(e) + } + ) + return status + + +class SystemMonitor: + def __init__(self): + self.components = {} + self.recent = deque(maxlen=10) + + def add_component(self, component: ComponentMonitor): + self.components[component.name] = component + return component + + async def request(self): + """ + Request status from each component. + """ + tasks = { + name: asyncio.create_task(comp.status()) + for name, comp in self.components.items() + } + await asyncio.gather(*tasks.values()) + status = { + name: await fut for name, fut in tasks.items() + } + self.recent.append(status) + return status + + async def _format_summary(self, status_dict: dict[str, ComponentStatus]): + """ + Format a one line summary from a status dict. + """ + freq = {level: 0 for level in StatusLevel} + for status in status_dict.values(): + freq[status.level] += 1 + + summary = '\t'.join(f"{level.symbol} {count}" for level, count in freq.items() if count) + return summary + + async def _format_overview(self, status_dict: dict[str, ComponentStatus]): + """ + Format an overview (one line per component) from a status dict. + """ + lines = [] + for name, status in status_dict.items(): + lines.append(f"{status.level.symbol} {name}: {status.short}") + summary = await self._format_summary(status_dict) + return '\n'.join((summary, *lines)) + + async def get_summary(self): + return await self._format_summary(await self.request()) + + async def get_overview(self): + return await self._format_overview(await self.request()) diff --git a/src/modules/pomodoro/cog.py b/src/modules/pomodoro/cog.py index f2a5cc41..fd25b318 100644 --- a/src/modules/pomodoro/cog.py +++ b/src/modules/pomodoro/cog.py @@ -10,6 +10,7 @@ from discord import app_commands as appcmds from meta import LionCog, LionBot, LionContext from meta.logger import log_wrap from meta.sharding import THIS_SHARD +from meta.monitor import ComponentMonitor, ComponentStatus, StatusLevel from utils.lib import utc_now from wards import low_management_ward @@ -42,12 +43,25 @@ class TimerCog(LionCog): self.bot = bot self.data = bot.db.load_registry(TimerData()) self.settings = TimerSettings() + self.monitor = ComponentMonitor('TimerCog', self._monitor) + self.timer_options = TimerOptions() self.ready = False self.timers = defaultdict(dict) + async def _monitor(self): + if not self.ready: + level = StatusLevel.STARTING + info = "(STARTING) Not ready. {timers} timers loaded." + else: + level = StatusLevel.OKAY + info = "(OK) {timers} timers loaded." + data = dict(timers=len(self.timers)) + return ComponentStatus(level, info, info, data) + async def cog_load(self): + self.bot.system_monitor.add_component(self.monitor) await self.data.init() self.bot.core.guild_config.register_model_setting(self.settings.PomodoroChannel) diff --git a/src/modules/schedule/cog.py b/src/modules/schedule/cog.py index 75d56642..39b60960 100644 --- a/src/modules/schedule/cog.py +++ b/src/modules/schedule/cog.py @@ -13,6 +13,7 @@ from meta import LionCog, LionBot, LionContext from meta.logger import log_wrap from meta.errors import UserInputError, ResponseTimedOut from meta.sharding import THIS_SHARD +from meta.monitor import ComponentMonitor, ComponentStatus, StatusLevel from utils.lib import utc_now, error_embed from utils.ui import Confirm from utils.data import MULTIVALUE_IN, MEMBERS @@ -38,6 +39,10 @@ class ScheduleCog(LionCog): self.bot = bot self.data: ScheduleData = bot.db.load_registry(ScheduleData()) self.settings = ScheduleSettings() + self.monitor = ComponentMonitor( + 'ScheduleCog', + self._monitor + ) # Whether we are ready to take events self.initialised = asyncio.Event() @@ -57,12 +62,56 @@ class ScheduleCog(LionCog): self.session_channels = self.settings.SessionChannels._cache + async def _monitor(self): + nowid = self.nowid + now = None + now_lock = self.slotlock(nowid) + if not self.initialised.is_set(): + level = StatusLevel.STARTING + info = ( + "(STARTING) " + "Not ready. " + "Spawn task is {spawn}. " + "Spawn lock is {spawn_lock}. " + "Active slots {active}." + ) + elif nowid not in self.active_slots: + level = StatusLevel.UNSURE + info = ( + "(UNSURE) " + "Setup, but current slotid {nowid} not active. " + "Spawn task is {spawn}. " + "Spawn lock is {spawn_lock}. " + "Now lock is {now_lock}. " + "Active slots {active}." + ) + else: + now = self.active_slots[nowid] + level = StatusLevel.OKAY + info = ( + "(OK) " + "Running current slot {now}. " + "Spawn lock is {spawn_lock}. " + "Now lock is {now_lock}. " + "Active slots {active}." + ) + data = { + 'spawn': self.spawn_task, + 'spawn_lock': self.spawn_lock, + 'active': self.active_slots, + 'nowid': nowid, + 'now_lock': now_lock, + 'now': now, + } + return ComponentStatus(level, info, info, data) + @property def nowid(self): now = utc_now() return time_to_slotid(now) async def cog_load(self): + self.bot.system_monitor.add_component(self.monitor) await self.data.init() # Update the session channel cache diff --git a/src/modules/sysadmin/exec_cog.py b/src/modules/sysadmin/exec_cog.py index 63b5c151..351219c5 100644 --- a/src/modules/sysadmin/exec_cog.py +++ b/src/modules/sysadmin/exec_cog.py @@ -186,6 +186,17 @@ def mk_print(fp: io.StringIO) -> Callable[..., None]: return _print +def mk_status_printer(bot, printer): + async def _status(details=False): + if details: + status = await bot.system_monitor.get_overview() + else: + status = await bot.system_monitor.get_summary() + printer(status) + return status + return _status + + @log_wrap(action="Code Exec") async def _async(to_eval: str, style='exec'): newline = '\n' * ('\n' in to_eval) @@ -202,6 +213,7 @@ async def _async(to_eval: str, style='exec'): scope['ctx'] = ctx = context.get() scope['bot'] = ctx_bot.get() scope['print'] = _print # type: ignore + scope['print_status'] = mk_status_printer(scope['bot'], _print) try: if ctx and ctx.message: @@ -297,7 +309,7 @@ class Exec(LionCog): file = discord.File(fp, filename=f"output-{target}.md") await ctx.reply(file=file) elif result: - await ctx.reply(f"```md{result}```") + await ctx.reply(f"```md\n{result}```") else: await ctx.reply("Command completed, and had no output.") else: @@ -351,7 +363,7 @@ class Exec(LionCog): except asyncio.TimeoutError: return if ctx.interaction: - await ctx.interaction.response.defer(thinking=True, ephemeral=True) + await ctx.interaction.response.defer(thinking=True) if target is not None: if target not in shard_talk.peers: embed = discord.Embed(description=f"Unknown peer {target}", colour=discord.Colour.red()) @@ -376,7 +388,7 @@ class Exec(LionCog): await ctx.reply(file=file) else: # Send as message - await ctx.reply(f"```md\n{output}```", ephemeral=True) + await ctx.reply(f"```md\n{output}```") asyncall_cmd.autocomplete('target')(_peer_acmpl) From 6a7e047833e59929358895f290612d18e5af7407 Mon Sep 17 00:00:00 2001 From: Conatum Date: Fri, 22 Sep 2023 10:50:41 +0300 Subject: [PATCH 17/25] fix(rmenus): Fix typo in addrole. --- src/modules/rolemenus/cog.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/rolemenus/cog.py b/src/modules/rolemenus/cog.py index 1f33bd56..a3cb9d6a 100644 --- a/src/modules/rolemenus/cog.py +++ b/src/modules/rolemenus/cog.py @@ -971,6 +971,8 @@ class RoleMenuCog(LionCog): ) # TODO: Generate the custom message from the template if it doesn't exist + # TODO: Pathway for setting menu style + if rawmessage is not None: msg_config = target.config.rawmessage content = await msg_config.download_attachment(rawmessage) @@ -1356,7 +1358,7 @@ class RoleMenuCog(LionCog): await target.update_message() if target_is_reaction: try: - await self.menu.update_reactons() + await target.update_reactons() except SafeCancellation as e: embed.add_field( name=t(_p( From a4643529db20dfa4a631f143a4d78c675821ee3f Mon Sep 17 00:00:00 2001 From: Conatum Date: Fri, 22 Sep 2023 12:05:27 +0300 Subject: [PATCH 18/25] fix(rmenus): Better handling of post errors. --- src/modules/rolemenus/cog.py | 9 +++++++-- src/modules/rolemenus/ui/menueditor.py | 18 +++++++----------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/modules/rolemenus/cog.py b/src/modules/rolemenus/cog.py index a3cb9d6a..3724cb76 100644 --- a/src/modules/rolemenus/cog.py +++ b/src/modules/rolemenus/cog.py @@ -976,11 +976,16 @@ class RoleMenuCog(LionCog): if rawmessage is not None: msg_config = target.config.rawmessage content = await msg_config.download_attachment(rawmessage) - data = await msg_config._parse_string(content) + data = await msg_config._parse_string(0, content) update_args[msg_config._column] = data if template is None: update_args[self.data.RoleMenu.templateid.name] = None - ack_lines.append(msg_config.update_message) + ack_lines.append( + t(_p( + 'cmd:rolemenu_edit|parse:custom_message|success', + "Custom menu message updated." + )) + ) # Update the data, if applicable if update_args: diff --git a/src/modules/rolemenus/ui/menueditor.py b/src/modules/rolemenus/ui/menueditor.py index 3af0101f..dca450b7 100644 --- a/src/modules/rolemenus/ui/menueditor.py +++ b/src/modules/rolemenus/ui/menueditor.py @@ -644,7 +644,7 @@ class MenuEditor(MessageUI): self.bot, json.loads(self.menu.data.rawmessage), callback=self._editor_callback, callerid=self._callerid ) self._slaves.append(editor) - await editor.run(interaction) + await editor.run(interaction, ephemeral=True) # Template/Custom Menu @select(cls=Select, placeholder="TEMPLATE_MENU_PLACEHOLDER", min_values=1, max_values=1) @@ -821,20 +821,15 @@ class MenuEditor(MessageUI): """ Display or update the preview message. """ - args = await self.menu.make_args() - view = await self.menu.make_view() if self._preview is not None: try: await self._preview.delete_original_response() except discord.HTTPException: pass self._preview = None - await press.response.send_message( - **args.send_args, - view=view or discord.utils.MISSING, - ephemeral=True - ) + await press.response.defer(thinking=True, ephemeral=True) self._preview = press + await self.update_preview() async def preview_button_refresh(self): t = self.bot.translator.t @@ -887,13 +882,14 @@ class MenuEditor(MessageUI): description=desc ) await selection.edit_original_response(embed=embed) - except discord.HTTPException: + except discord.HTTPException as e: error = discord.Embed( colour=discord.Colour.brand_red(), description=t(_p( 'ui:menu_editor|button:repost|widget:repost|error:post_failed', - "An error ocurred while posting to {channel}. Do I have sufficient permissions?" - )).format(channel=channel.mention) + "An unknown error ocurred while posting to {channel}!\n" + "**Error:** `{exception}`" + )).format(channel=channel.mention, exception=e.text) ) await selection.edit_original_response(embed=error) else: From 5869c591f267454a27906f73d7ff54a2ed159fc5 Mon Sep 17 00:00:00 2001 From: Conatum Date: Fri, 22 Sep 2023 23:13:55 +0300 Subject: [PATCH 19/25] fix(rmenus): Pass guildid to role parser. --- src/modules/rolemenus/ui/menueditor.py | 9 +++++++-- src/settings/setting_types.py | 7 +++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/modules/rolemenus/ui/menueditor.py b/src/modules/rolemenus/ui/menueditor.py index dca450b7..356f42a0 100644 --- a/src/modules/rolemenus/ui/menueditor.py +++ b/src/modules/rolemenus/ui/menueditor.py @@ -190,7 +190,10 @@ class MenuEditor(MessageUI): if not userstr: new_data = None else: - new_data = await instance._parse_string(instance.parent_id, userstr) + new_data = await instance._parse_string( + instance.parent_id, userstr, + guildid=self.menu.data.guildid + ) instance.data = new_data modified.append(instance) if modified: @@ -349,7 +352,9 @@ class MenuEditor(MessageUI): if not userstr: new_data = None else: - new_data = await instance._parse_string(instance.parent_id, userstr, interaction=interaction) + new_data = await instance._parse_string( + instance.parent_id, userstr, interaction=interaction + ) instance.data = new_data modified.append(instance) if modified: diff --git a/src/settings/setting_types.py b/src/settings/setting_types.py index 2dd67815..f5c99c5b 100644 --- a/src/settings/setting_types.py +++ b/src/settings/setting_types.py @@ -396,7 +396,7 @@ class RoleSetting(InteractiveSetting[ParentID, int, Union[discord.Role, discord. if data is not None: role = None - guildid = cls._get_guildid(parent_id) + guildid = cls._get_guildid(parent_id, **kwargs) bot = ctx_bot.get() guild = bot.get_guild(guildid) if guild is not None: @@ -409,11 +409,14 @@ class RoleSetting(InteractiveSetting[ParentID, int, Union[discord.Role, discord. async def _parse_string(cls, parent_id, string: str, **kwargs): if not string or string.lower() == 'none': return None + guildid = cls._get_guildid(parent_id, **kwargs) t = ctx_translator.get().t bot = ctx_bot.get() role = None - guild = bot.get_guild(parent_id) + guild = bot.get_guild(guildid) + if guild is None: + raise ValueError("Attempting to parse role string with no guild.") if string.isdigit(): maybe_id = int(string) From 576d7cf02f39db6aec4ce69acce3f591b51ebae4 Mon Sep 17 00:00:00 2001 From: Conatum Date: Sat, 23 Sep 2023 00:50:17 +0300 Subject: [PATCH 20/25] fix(rmenus): Make repost ephemeral. --- src/modules/rolemenus/ui/menueditor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/rolemenus/ui/menueditor.py b/src/modules/rolemenus/ui/menueditor.py index 356f42a0..55c949e3 100644 --- a/src/modules/rolemenus/ui/menueditor.py +++ b/src/modules/rolemenus/ui/menueditor.py @@ -949,7 +949,7 @@ class MenuEditor(MessageUI): title=title, description=desc ) # Send as response with the repost widget attached - await press.response.send_message(embed=embed, view=AsComponents(repost_widget)) + await press.response.send_message(embed=embed, view=AsComponents(repost_widget), ephemeral=True) async def repost_button_refresh(self): t = self.bot.translator.t @@ -1040,7 +1040,7 @@ class MenuEditor(MessageUI): role_index = int(splits[i-1]) mrole = self.menu.roles[role_index] - error = discord.Embed( + embed = discord.Embed( colour=discord.Colour.brand_red(), title=t(_p( 'ui:menu_editor|error:invald_emoji|title', @@ -1052,7 +1052,7 @@ class MenuEditor(MessageUI): )).format(emoji=mrole.config.emoji.data, label=mrole.config.label.data) ) await mrole.data.update(emoji=None) - await self.channel.send(embed=error) + await self.channel.send(embed=embed) async def _redraw(self, args): try: From 5675f728532bf106de85391bd5745a7c4c6d88c6 Mon Sep 17 00:00:00 2001 From: Conatum Date: Sat, 23 Sep 2023 15:28:12 +0300 Subject: [PATCH 21/25] feat(rmenus): Support negative prices. --- src/core/setting_types.py | 20 ++++++++----- src/modules/rolemenus/cog.py | 4 +-- src/modules/rolemenus/rolemenu.py | 15 +++++++--- src/modules/rolemenus/roleoptions.py | 45 +++++++++++++++++++--------- 4 files changed, 56 insertions(+), 28 deletions(-) diff --git a/src/core/setting_types.py b/src/core/setting_types.py index 7bc1710d..96c31364 100644 --- a/src/core/setting_types.py +++ b/src/core/setting_types.py @@ -49,16 +49,20 @@ class CoinSetting(IntegerSetting): if num > cls._max: t = ctx_translator.get().t - raise UserInputError(t(_p( - 'settype:coin|parse|error:too_large', - "Provided number of coins was too high!" - ))) from None + raise UserInputError( + t(_p( + 'settype:coin|parse|error:too_large', + "You cannot set this to more than {coin}**{max}**!" + )).format(coin=conf.emojis.coin, max=cls._max) + ) from None elif num < cls._min: t = ctx_translator.get().t - raise UserInputError(t(_p( - 'settype:coin|parse|error:too_large', - "Provided number of coins was too low!" - ))) from None + raise UserInputError( + t(_p( + 'settype:coin|parse|error:too_small', + "You cannot set this to less than {coin}**{min}**!" + )).format(coin=conf.emojis.coin, min=cls._min) + ) from None return num diff --git a/src/modules/rolemenus/cog.py b/src/modules/rolemenus/cog.py index 3724cb76..3618b4fd 100644 --- a/src/modules/rolemenus/cog.py +++ b/src/modules/rolemenus/cog.py @@ -1192,7 +1192,7 @@ class RoleMenuCog(LionCog): label: Optional[appcmds.Range[str, 1, 100]] = None, emoji: Optional[appcmds.Range[str, 0, 100]] = None, description: Optional[appcmds.Range[str, 0, 100]] = None, - price: Optional[appcmds.Range[int, 0, MAX_COINS]] = None, + price: Optional[appcmds.Range[int, -MAX_COINS, MAX_COINS]] = None, duration: Optional[Transform[int, DurationTransformer(60)]] = None, ): # Type checking guards @@ -1448,7 +1448,7 @@ class RoleMenuCog(LionCog): label: Optional[appcmds.Range[str, 1, 100]] = None, emoji: Optional[appcmds.Range[str, 0, 100]] = None, description: Optional[appcmds.Range[str, 0, 100]] = None, - price: Optional[appcmds.Range[int, 0, MAX_COINS]] = None, + price: Optional[appcmds.Range[int, -MAX_COINS, MAX_COINS]] = None, duration: Optional[Transform[int, DurationTransformer(60)]] = None, ): # Type checking wards diff --git a/src/modules/rolemenus/rolemenu.py b/src/modules/rolemenus/rolemenu.py index da25d30c..fcaafdcf 100644 --- a/src/modules/rolemenus/rolemenu.py +++ b/src/modules/rolemenus/rolemenu.py @@ -165,7 +165,7 @@ class RoleMenu: await menu.attach() return menu - async def fetch_message(self, refresh=False): + async def fetch_message(self, refresh=False) -> Optional[discord.Message]: """ Fetch the message the menu is attached to. """ @@ -529,11 +529,17 @@ class RoleMenu: "Role removed" )) ) - if total_refund: + if total_refund > 0: embed.description = t(_p( 'rolemenu|deselect|success:refund|desc', "You have removed **{role}**, and been refunded {coin} **{amount}**." )).format(role=role.name, coin=self.bot.config.emojis.coin, amount=total_refund) + if total_refund < 0: + # TODO: Consider disallowing them from removing roles if their balance would go negative + embed.description = t(_p( + 'rolemenu|deselect|success:negrefund|desc', + "You have removed **{role}**, and have lost {coin} **{amount}**." + )).format(role=role.name, coin=self.bot.config.emojis.coin, amount=-total_refund) else: embed.description = t(_p( 'rolemenu|deselect|success:norefund|desc', @@ -551,7 +557,7 @@ class RoleMenu: raise UserInputError( t(_p( 'rolemenu|select|error:required_role', - "You need to have the **{role}** role to use this!" + "You need to have the role **{role}** required to use this menu!" )).format(role=name) ) @@ -647,7 +653,7 @@ class RoleMenu: "Role equipped" )) ) - if price > 0: + if price: embed.description = t(_p( 'rolemenu|select|success:purchase|desc', "You have purchased the role **{role}** for {coin}**{amount}**" @@ -665,6 +671,7 @@ class RoleMenu: )).format( timestamp=discord.utils.format_dt(expiry) ) + # TODO Event logging return embed async def interactive_selection(self, interaction: discord.Interaction, menuroleid: int): diff --git a/src/modules/rolemenus/roleoptions.py b/src/modules/rolemenus/roleoptions.py index e3ab699c..bdcc8d89 100644 --- a/src/modules/rolemenus/roleoptions.py +++ b/src/modules/rolemenus/roleoptions.py @@ -1,14 +1,17 @@ +from typing import Optional import discord from settings import ModelData from settings.groups import SettingGroup, ModelConfig, SettingDotDict from settings.setting_types import ( - RoleSetting, BoolSetting, StringSetting, DurationSetting, EmojiSetting + RoleSetting, StringSetting, DurationSetting, EmojiSetting ) from core.setting_types import CoinSetting from utils.ui import AButton, AsComponents from meta.errors import UserInputError +from meta import conf from babel.translator import ctx_translator +from constants import MAX_COINS from .data import RoleMenuData from . import babel @@ -74,6 +77,9 @@ class RoleMenuRoleOptions(SettingGroup): "This menu item will now give the role {role}." )).format(role=self.formatted) return resp + else: + raise ValueError("Attempt to call update_message without a value.") + @RoleMenuRoleConfig.register_model_setting class Label(ModelData, StringSetting): @@ -138,7 +144,9 @@ class RoleMenuRoleOptions(SettingGroup): return button @classmethod - async def _parse_string(cls, parent_id, string: str, interaction: discord.Interaction = None, **kwargs): + async def _parse_string(cls, parent_id, string: str, + interaction: Optional[discord.Interaction] = None, + **kwargs): emojistr = await super()._parse_string(parent_id, string, interaction=interaction, **kwargs) if emojistr and interaction is not None: # Use the interaction to test @@ -151,7 +159,7 @@ class RoleMenuRoleOptions(SettingGroup): view=view, ) except discord.HTTPException: - t = interaction.client.translator.t + t = ctx_translator.get().t raise UserInputError(t(_p( 'roleset:emoji|error:test_emoji', "The selected emoji `{emoji}` is invalid or has been deleted." @@ -218,34 +226,43 @@ class RoleMenuRoleOptions(SettingGroup): _display_name = _p('roleset:price', "price") _desc = _p( 'roleset:price|desc', - "Price of the role, in LionCoins." + "Price of the role, in LionCoins. May be negative." ) _long_desc = _p( 'roleset:price|long_desc', - "How much the role costs when selected, in LionCoins." + "How many LionCoins should be deducted from a member's account " + "when they equip this role through this menu.\n" + "The price may be negative, in which case the member will instead be rewarded " + "coins when they equip the role." ) _accepts = _p( 'roleset:price|accepts', - "Amount of coins that the role costs." + "Amount of coins that the role costs when equipped." ) _default = 0 + _min = - MAX_COINS _model = RoleMenuData.RoleMenuRole _column = RoleMenuData.RoleMenuRole.price.name @property def update_message(self) -> str: t = ctx_translator.get().t - value = self.value - if value: + value = self.value or 0 + if value > 0: resp = t(_p( - 'roleset:price|set_response:set', - "This role will now cost {price} to equip." - )).format(price=self.formatted) + 'roleset:price|set_response:positive', + "Equipping this role will now cost {coin}**{price}**." + )).format(price=value, coin=conf.emojis.coin) + elif value == 0: + resp = t(_p( + 'roleset:price|set_response:zero', + "Equipping this role is now free." + )) else: resp = t(_p( - 'roleset:price|set_response:unset', - "This role will now be free to equip from this role menu." - )) + 'roleset:price|set_response:negative', + "Equipping this role will now reward {coin}**{price}**." + )).format(price=-value, coin=conf.emojis.coin) return resp @RoleMenuRoleConfig.register_model_setting From fe0d12090724adbdf20761cf8dcf541e2f8c10a8 Mon Sep 17 00:00:00 2001 From: Conatum Date: Sun, 24 Sep 2023 09:22:50 +0300 Subject: [PATCH 22/25] fix(ranks): Fix typos in refresh. --- src/modules/ranks/cog.py | 10 +++++----- src/modules/ranks/ui/refresh.py | 10 ++++++++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/modules/ranks/cog.py b/src/modules/ranks/cog.py index f7878c15..76e615bf 100644 --- a/src/modules/ranks/cog.py +++ b/src/modules/ranks/cog.py @@ -622,7 +622,7 @@ class RankCog(LionCog): error = t(_p( 'rank_refresh|error:unassignable_roles|desc', "I have insufficient permissions to assign the following role(s):\n{roles}" - )).format(roles='\n'.join(role.mention for role in failing)), + )).format(roles='\n'.join(role.mention for role in failing)) await ui.set_error(error) return @@ -707,8 +707,8 @@ class RankCog(LionCog): 'rank_refresh|remove_roles|small_error', "*Could not remove ranks from {member}*" )).format(member=to_remove[index][0].mention) - self.ui.errors.append(error) - if len(self.ui.errors) > 10: + ui.errors.append(error) + if len(ui.errors) > 10: await ui.set_error( t(_p( 'rank_refresh|remove_roles|error:too_many_issues', @@ -742,8 +742,8 @@ class RankCog(LionCog): 'rank_refresh|add_roles|small_error', "*Could not add {role} to {member}*" )).format(member=to_add[index][0].mention, role=to_add[index][1].mention) - self.ui.errors.append(error) - if len(self.ui.errors) > 10: + ui.errors.append(error) + if len(ui.errors) > 10: await ui.set_error( t(_p( 'rank_refresh|add_roles|error:too_many_issues', diff --git a/src/modules/ranks/ui/refresh.py b/src/modules/ranks/ui/refresh.py index b45b3b6e..4b30b256 100644 --- a/src/modules/ranks/ui/refresh.py +++ b/src/modules/ranks/ui/refresh.py @@ -24,6 +24,9 @@ _p = babel._p class RankRefreshUI(MessageUI): + # Cache of live rank UIs, mainly for introspection + _running = set() + def __init__(self, bot: LionBot, guild: discord.Guild, **kwargs): super().__init__(**kwargs) self.bot = bot @@ -66,6 +69,7 @@ class RankRefreshUI(MessageUI): def start(self): self._loop_task = asyncio.create_task(self._refresh_loop(), name='Rank RefreshUI Monitor') + self._running.add(self) async def run(self, *args, **kwargs): await super().run(*args, **kwargs) @@ -74,6 +78,7 @@ class RankRefreshUI(MessageUI): async def cleanup(self): if self._loop_task and not self._loop_task.done(): self._loop_task.cancel() + self._running.discard(self) await super().cleanup() def progress_bar(self, value, minimum, maximum, width=10) -> str: @@ -107,12 +112,13 @@ class RankRefreshUI(MessageUI): # Join all the sections together and return return ''.join(bar) - @log_wrap('refresh ui loop') + @log_wrap(action='refresh ui loop') async def _refresh_loop(self): while True: try: - await asyncio.sleep(1) + await asyncio.sleep(5) await self._wakeup.wait() + self._wakeup.clear() await self.refresh() except asyncio.CancelledError: break From a7663f9267b715c12f080be2e9056ba7eb50e0de Mon Sep 17 00:00:00 2001 From: Conatum Date: Sun, 24 Sep 2023 11:22:17 +0300 Subject: [PATCH 23/25] fix(logger): Livelogging ratelimit prevention. --- src/meta/logger.py | 33 ++++++++++++++++++++++----------- src/utils/ratelimits.py | 4 ++++ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/meta/logger.py b/src/meta/logger.py index b2464088..ffa97f7f 100644 --- a/src/meta/logger.py +++ b/src/meta/logger.py @@ -10,6 +10,7 @@ from io import StringIO from functools import wraps from contextvars import ContextVar +import discord from discord import Webhook, File import aiohttp @@ -304,6 +305,10 @@ class WebHookHandler(logging.StreamHandler): self.webhook = Webhook.from_url(self.webhook_url, session=self.session) async def post(self, record): + if record.context == 'Webhook Logger': + # Don't livelog livelog errors + # Otherwise we recurse and Cloudflare hates us + return log_context.set("Webhook Logger") log_action_stack.set(("Logging",)) log_app.set(record.app) @@ -371,7 +376,7 @@ class WebHookHandler(logging.StreamHandler): except BucketFull: logger.warning( "Can't keep up! " - "Ignoring records on live-logger {self.webhook.id}." + f"Ignoring records on live-logger {self.webhook.id}." ) self.ignored += 1 return @@ -383,16 +388,22 @@ class WebHookHandler(logging.StreamHandler): ) self.ignored = 0 - if as_file or len(message) > 1900: - with StringIO(message) as fp: - fp.seek(0) - await self.webhook.send( - f"{self.prefix}\n`{message.splitlines()[0]}`", - file=File(fp, filename="logs.md"), - username=log_app.get() - ) - else: - await self.webhook.send(self.prefix + '\n' + message, username=log_app.get()) + try: + if as_file or len(message) > 1900: + with StringIO(message) as fp: + fp.seek(0) + await self.webhook.send( + f"{self.prefix}\n`{message.splitlines()[0]}`", + file=File(fp, filename="logs.md"), + username=log_app.get() + ) + else: + await self.webhook.send(self.prefix + '\n' + message, username=log_app.get()) + except discord.HTTPException: + logger.exception( + "Live logger errored. Slowing down live logger." + ) + self.bucket.fill() handlers = [] diff --git a/src/utils/ratelimits.py b/src/utils/ratelimits.py index 56f25728..4c050e94 100644 --- a/src/utils/ratelimits.py +++ b/src/utils/ratelimits.py @@ -80,6 +80,10 @@ class Bucket: self._last_full = False self._level += 1 + def fill(self): + self._leak() + self._level = max(self._level, self.max_level + 1) + async def wait(self): """ Wait until the bucket has room. From 245b7dbd7ad70872d7a3f6f5a6531cb89901d360 Mon Sep 17 00:00:00 2001 From: Conatum Date: Sun, 24 Sep 2023 11:51:23 +0300 Subject: [PATCH 24/25] fix(rmenus): Update reactions on style change. --- src/modules/rolemenus/ui/menueditor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/modules/rolemenus/ui/menueditor.py b/src/modules/rolemenus/ui/menueditor.py index 55c949e3..a4c5f55f 100644 --- a/src/modules/rolemenus/ui/menueditor.py +++ b/src/modules/rolemenus/ui/menueditor.py @@ -508,6 +508,11 @@ class MenuEditor(MessageUI): await self.refresh(thinking=selection) await self.update_preview() await self.menu.update_message() + if self.menu.data.menutype is MenuType.REACTION: + try: + await self.menu.update_reactons() + except SafeCancellation: + pass else: await selection.response.defer(thinking=False) @@ -596,6 +601,8 @@ class MenuEditor(MessageUI): await self.refresh(thinking=selection) await self.update_preview() await self.menu.update_message() + if menutype is MenuType.REACTION: + await self.menu.update_reactons() else: await selection.response.defer() From aac560958f6e77901570eee5521832a06e12bfa2 Mon Sep 17 00:00:00 2001 From: Conatum Date: Sun, 24 Sep 2023 12:22:23 +0300 Subject: [PATCH 25/25] chore(locales): Regenerate string templates. --- locales/templates/Pomodoro.pot | 130 +++++----- locales/templates/babel.pot | 220 +++++++++-------- locales/templates/config.pot | 2 +- locales/templates/core_config.pot | 2 +- locales/templates/economy.pot | 2 +- locales/templates/exec.pot | 34 +-- locales/templates/goals-gui.pot | 2 +- locales/templates/leaderboard-gui.pot | 2 +- locales/templates/lion-core.pot | 42 ++-- locales/templates/member_admin.pot | 24 +- locales/templates/meta.pot | 2 +- locales/templates/moderation.pot | 2 +- locales/templates/monthly-gui.pot | 2 +- locales/templates/profile-gui.pot | 2 +- locales/templates/ranks.pot | 227 ++++++++--------- locales/templates/reminders.pot | 2 +- locales/templates/rolemenus.pot | 335 ++++++++++++++------------ locales/templates/rooms.pot | 2 +- locales/templates/schedule.pot | 68 +++--- locales/templates/settings_base.pot | 52 ++-- locales/templates/shop.pot | 12 +- locales/templates/statistics.pot | 69 +++--- locales/templates/stats-gui.pot | 2 +- locales/templates/sysadmin.pot | 2 +- locales/templates/tasklist.pot | 2 +- locales/templates/test.pot | 2 +- locales/templates/text-tracker.pot | 2 +- locales/templates/timer-gui.pot | 2 +- locales/templates/user_config.pot | 2 +- locales/templates/utils.pot | 2 +- locales/templates/video.pot | 2 +- locales/templates/voice-tracker.pot | 2 +- locales/templates/wards.pot | 26 +- locales/templates/weekly-gui.pot | 2 +- 34 files changed, 651 insertions(+), 632 deletions(-) diff --git a/locales/templates/Pomodoro.pot b/locales/templates/Pomodoro.pot index 106d1b03..7c2448c5 100644 --- a/locales/templates/Pomodoro.pot +++ b/locales/templates/Pomodoro.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,41 +18,41 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" -#: src/modules/pomodoro/timer.py:51 +#: src/modules/pomodoro/timer.py:52 msgctxt "timer|stage:break|name" msgid "BREAK" msgstr "" -#: src/modules/pomodoro/timer.py:52 +#: src/modules/pomodoro/timer.py:53 msgctxt "timer|stage:focus|name" msgid "FOCUS" msgstr "" -#: src/modules/pomodoro/timer.py:158 +#: src/modules/pomodoro/timer.py:160 #, possible-python-brace-format msgctxt "timer|webhook|name" msgid "{bot_name} Pomodoro" msgstr "" -#: src/modules/pomodoro/timer.py:162 +#: src/modules/pomodoro/timer.py:164 msgctxt "timer|webhook|audit_reason" msgid "Pomodoro Notifications" msgstr "" -#: src/modules/pomodoro/timer.py:173 +#: src/modules/pomodoro/timer.py:175 msgctxt "timer|webhook|error:insufficient_permissions" msgid "" "I require the `MANAGE_WEBHOOKS` permission to send pomodoro notifications " "here!" msgstr "" -#: src/modules/pomodoro/timer.py:232 +#: src/modules/pomodoro/timer.py:234 #, possible-python-brace-format msgctxt "timer|default_base_name" msgid "Timer {pattern}" msgstr "" -#: src/modules/pomodoro/timer.py:406 +#: src/modules/pomodoro/timer.py:408 #, possible-python-brace-format msgctxt "timer|kicked_message" msgid "" @@ -64,20 +64,20 @@ msgid_plural "" msgstr[0] "" msgstr[1] "" -#: src/modules/pomodoro/timer.py:499 +#: src/modules/pomodoro/timer.py:501 #, possible-python-brace-format msgctxt "timer|status|stage:focus|statusline" msgid "{channel} is now in **FOCUS**! Good luck, **BREAK** starts {timestamp}" msgstr "" -#: src/modules/pomodoro/timer.py:504 +#: src/modules/pomodoro/timer.py:506 #, possible-python-brace-format msgctxt "timer|status|stage:break|statusline" msgid "" "{channel} is now on **BREAK**! Take a rest, **FOCUS** starts {timestamp}" msgstr "" -#: src/modules/pomodoro/timer.py:536 +#: src/modules/pomodoro/timer.py:538 #, possible-python-brace-format msgctxt "timer|status|warningline" msgid "" @@ -85,13 +85,13 @@ msgid "" "next stage." msgstr "" -#: src/modules/pomodoro/timer.py:555 +#: src/modules/pomodoro/timer.py:557 #, possible-python-brace-format msgctxt "timer|status|stopped:auto" msgid "Timer stopped! Join {channel} to start the timer." msgstr "" -#: src/modules/pomodoro/timer.py:560 +#: src/modules/pomodoro/timer.py:562 msgctxt "timer|status|stopped:manual" msgid "Timer stopped! Press `Start` to restart the timer." msgstr "" @@ -116,34 +116,34 @@ msgctxt "dash:stats|dropdown|placeholder" msgid "Pomodoro Timer Panel" msgstr "" -#: src/modules/pomodoro/cog.py:82 +#: src/modules/pomodoro/cog.py:96 msgctxt "cmd_check:ready|failed" msgid "" "I am currently restarting! The Pomodoro timers will be unavailable until I " "have restarted. Thank you for your patience!" msgstr "" -#: src/modules/pomodoro/cog.py:322 +#: src/modules/pomodoro/cog.py:336 msgctxt "cmd:timer" msgid "timer" msgstr "" -#: src/modules/pomodoro/cog.py:323 +#: src/modules/pomodoro/cog.py:337 msgctxt "cmd:timer|desc" msgid "Show your current (or selected) pomodoro timer." msgstr "" -#: src/modules/pomodoro/cog.py:326 +#: src/modules/pomodoro/cog.py:340 msgctxt "cmd:timer|param:channel" msgid "timer_channel" msgstr "" -#: src/modules/pomodoro/cog.py:331 +#: src/modules/pomodoro/cog.py:345 msgctxt "cmd:timer|param:channel|desc" msgid "Select a timer to display (by selecting the timer voice channel)" msgstr "" -#: src/modules/pomodoro/cog.py:353 src/modules/pomodoro/cog.py:423 +#: src/modules/pomodoro/cog.py:367 src/modules/pomodoro/cog.py:438 #, possible-python-brace-format msgctxt "cmd:timer|error:no_timers|desc" msgid "" @@ -152,7 +152,7 @@ msgid "" "rent a private room with {room_cmd} and create one yourself!" msgstr "" -#: src/modules/pomodoro/cog.py:367 +#: src/modules/pomodoro/cog.py:381 #, possible-python-brace-format msgctxt "cmd:timer|error:no_channel|desc" msgid "" @@ -161,7 +161,7 @@ msgid "" "list the available timers in this server." msgstr "" -#: src/modules/pomodoro/cog.py:380 +#: src/modules/pomodoro/cog.py:394 #, possible-python-brace-format msgctxt "cmd:timer|error:no_timer_in_channel" msgid "" @@ -169,17 +169,17 @@ msgid "" "Use {timers_cmd} to list the available timers in this server." msgstr "" -#: src/modules/pomodoro/cog.py:396 +#: src/modules/pomodoro/cog.py:411 msgctxt "cmd:timers" msgid "timers" msgstr "" -#: src/modules/pomodoro/cog.py:397 +#: src/modules/pomodoro/cog.py:412 msgctxt "cmd:timers|desc" msgid "List the available pomodoro timer rooms." msgstr "" -#: src/modules/pomodoro/cog.py:436 +#: src/modules/pomodoro/cog.py:451 #, possible-python-brace-format msgctxt "cmd:timer|error:no_visible_timers|desc" msgid "" @@ -188,13 +188,13 @@ msgid "" "with {room_cmd} and create one yourself!" msgstr "" -#: src/modules/pomodoro/cog.py:449 +#: src/modules/pomodoro/cog.py:464 #, possible-python-brace-format msgctxt "cmd:timers|embed:timer_list|title" msgid "Pomodoro Timer Rooms in **{guild}**" msgstr "" -#: src/modules/pomodoro/cog.py:458 +#: src/modules/pomodoro/cog.py:473 #, possible-python-brace-format msgctxt "cmd:timers|status:stopped_auto" msgid "" @@ -202,7 +202,7 @@ msgid "" "Join {channel} to restart it." msgstr "" -#: src/modules/pomodoro/cog.py:464 +#: src/modules/pomodoro/cog.py:479 #, possible-python-brace-format msgctxt "cmd:timers|status:stopped_manual" msgid "" @@ -210,7 +210,7 @@ msgid "" "Join {channel} and press `Start` to start it!" msgstr "" -#: src/modules/pomodoro/cog.py:471 +#: src/modules/pomodoro/cog.py:486 #, possible-python-brace-format msgctxt "cmd:timers|status:running_focus" msgid "" @@ -218,7 +218,7 @@ msgid "" "Currently **focusing**, with break starting {timestamp}" msgstr "" -#: src/modules/pomodoro/cog.py:477 +#: src/modules/pomodoro/cog.py:492 #, possible-python-brace-format msgctxt "cmd:timers|status:running_break" msgid "" @@ -226,78 +226,78 @@ msgid "" "Currently **resting**, with focus starting {timestamp}" msgstr "" -#: src/modules/pomodoro/cog.py:491 +#: src/modules/pomodoro/cog.py:506 msgctxt "cmd:pomodoro" msgid "pomodoro" msgstr "" -#: src/modules/pomodoro/cog.py:492 +#: src/modules/pomodoro/cog.py:507 msgctxt "cmd:pomodoro|desc" msgid "Create and configure pomodoro timer rooms." msgstr "" -#: src/modules/pomodoro/cog.py:499 +#: src/modules/pomodoro/cog.py:514 msgctxt "cmd:pomodoro_create" msgid "create" msgstr "" -#: src/modules/pomodoro/cog.py:502 +#: src/modules/pomodoro/cog.py:517 msgctxt "cmd:pomodoro_create|desc" msgid "Create a new Pomodoro timer. Requires manage channel permissions." msgstr "" -#: src/modules/pomodoro/cog.py:506 +#: src/modules/pomodoro/cog.py:521 msgctxt "cmd:pomodoro_create|param:channel" msgid "timer_channel" msgstr "" -#: src/modules/pomodoro/cog.py:512 +#: src/modules/pomodoro/cog.py:527 msgctxt "cmd:pomodoro_create|param:channel|desc" msgid "" "Voice channel to create the timer in. (Defaults to your current channel, or " "makes a new one.)" msgstr "" -#: src/modules/pomodoro/cog.py:557 +#: src/modules/pomodoro/cog.py:572 msgctxt "cmd:pomodoro_create|new_channel|error:your_insufficient_perms|title" msgid "Could not create pomodoro voice channel!" msgstr "" -#: src/modules/pomodoro/cog.py:561 +#: src/modules/pomodoro/cog.py:576 msgctxt "cmd:pomodoro_create|new_channel|error:your_insufficient_perms" msgid "" "No `timer_channel` was provided, and you lack the 'Manage Channels` " "permission required to create a new timer room!" msgstr "" -#: src/modules/pomodoro/cog.py:572 +#: src/modules/pomodoro/cog.py:587 msgctxt "cmd:pomodoro_create|new_channel|error:my_insufficient_perms|title" msgid "Could not create pomodoro voice channel!" msgstr "" -#: src/modules/pomodoro/cog.py:576 +#: src/modules/pomodoro/cog.py:591 msgctxt "cmd:pomodoro_create|new_channel|error:my_insufficient_perms|desc" msgid "" "No `timer_channel` was provided, and I lack the 'Manage Channels' permission " "required to create a new voice channel." msgstr "" -#: src/modules/pomodoro/cog.py:587 +#: src/modules/pomodoro/cog.py:602 msgctxt "cmd:pomodoro_create|new_channel|default_name" msgid "Timer" msgstr "" -#: src/modules/pomodoro/cog.py:591 +#: src/modules/pomodoro/cog.py:606 msgctxt "cmd:pomodoro_create|new_channel|audit_reason" msgid "Creating Pomodoro Voice Channel" msgstr "" -#: src/modules/pomodoro/cog.py:600 +#: src/modules/pomodoro/cog.py:615 msgctxt "cmd:pomodoro_create|new_channel|error:channel_create_failed|title" msgid "Could not create pomodoro voice channel!" msgstr "" -#: src/modules/pomodoro/cog.py:604 +#: src/modules/pomodoro/cog.py:619 msgctxt "cmd:pomodoro_create|new_channel|error:channel_create_failed|desc" msgid "" "Failed to create a new pomodoro voice channel due to an unknown Discord " @@ -305,13 +305,13 @@ msgid "" "the `timer_channel` argument of this command." msgstr "" -#: src/modules/pomodoro/cog.py:621 +#: src/modules/pomodoro/cog.py:636 #, possible-python-brace-format msgctxt "cmd:pomodoro_create|add_timer|error:timer_exists" msgid "A timer already exists in {channel}! Reconfigure it with {edit_cmd}." msgstr "" -#: src/modules/pomodoro/cog.py:635 +#: src/modules/pomodoro/cog.py:650 #, possible-python-brace-format msgctxt "cmd:pomodoro_create|add_timer|error:your_insufficient_perms" msgid "" @@ -319,43 +319,43 @@ msgid "" "timer there!" msgstr "" -#: src/modules/pomodoro/cog.py:684 +#: src/modules/pomodoro/cog.py:699 msgctxt "cmd:pomodoro_create|response:success|content" msgid "Timer created successfully! Use the panel below to reconfigure." msgstr "" -#: src/modules/pomodoro/cog.py:690 +#: src/modules/pomodoro/cog.py:705 msgctxt "cmd:pomodoro_destroy" msgid "destroy" msgstr "" -#: src/modules/pomodoro/cog.py:693 +#: src/modules/pomodoro/cog.py:708 msgctxt "cmd:pomodoro_destroy|desc" msgid "Remove a pomodoro timer from a voice channel." msgstr "" -#: src/modules/pomodoro/cog.py:697 +#: src/modules/pomodoro/cog.py:712 msgctxt "cmd:pomodoro_destroy|param:channel" msgid "timer_channel" msgstr "" -#: src/modules/pomodoro/cog.py:700 +#: src/modules/pomodoro/cog.py:715 msgctxt "cmd:pomodoro_destroy|param:channel" msgid "Select a timer voice channel to remove the timer from." msgstr "" -#: src/modules/pomodoro/cog.py:718 +#: src/modules/pomodoro/cog.py:733 msgctxt "cmd:pomodoro_destroy|error:no_timer" msgid "This channel doesn't have an attached pomodoro timer!" msgstr "" -#: src/modules/pomodoro/cog.py:731 +#: src/modules/pomodoro/cog.py:746 msgctxt "cmd:pomodoro_destroy|error:insufficient_perms|owned" msgid "" "You need to be an administrator or own this channel to remove this timer!" msgstr "" -#: src/modules/pomodoro/cog.py:740 +#: src/modules/pomodoro/cog.py:755 #, possible-python-brace-format msgctxt "cmd:pomodoro_destroy|error:insufficient_perms|notowned" msgid "" @@ -363,38 +363,38 @@ msgid "" "this timer!" msgstr "" -#: src/modules/pomodoro/cog.py:751 +#: src/modules/pomodoro/cog.py:766 #, possible-python-brace-format msgctxt "cmd:pomdoro_destroy|response:success|description" msgid "Timer successfully removed from {channel}." msgstr "" -#: src/modules/pomodoro/cog.py:757 +#: src/modules/pomodoro/cog.py:772 msgctxt "cmd:pomodoro_edit" msgid "edit" msgstr "" -#: src/modules/pomodoro/cog.py:760 +#: src/modules/pomodoro/cog.py:775 msgctxt "cmd:pomodoro_edit|desc" msgid "Reconfigure a pomodoro timer." msgstr "" -#: src/modules/pomodoro/cog.py:764 +#: src/modules/pomodoro/cog.py:779 msgctxt "cmd:pomodoro_edit|param:channel" msgid "timer_channel" msgstr "" -#: src/modules/pomodoro/cog.py:770 +#: src/modules/pomodoro/cog.py:785 msgctxt "cmd:pomodoro_edit|param:channel|desc" msgid "Select a timer voice channel to reconfigure." msgstr "" -#: src/modules/pomodoro/cog.py:811 +#: src/modules/pomodoro/cog.py:826 msgctxt "cmd:pomodoro_edit|error:no_timer" msgid "This channel doesn't have an attached pomodoro timer to edit!" msgstr "" -#: src/modules/pomodoro/cog.py:824 +#: src/modules/pomodoro/cog.py:839 msgctxt "cmd:pomodoro_edit|error:insufficient_perms|role:other" msgid "" "Insufficient permissions to modifiy this timer!\n" @@ -402,28 +402,28 @@ msgid "" "manager role." msgstr "" -#: src/modules/pomodoro/cog.py:845 +#: src/modules/pomodoro/cog.py:860 msgctxt "cmd:pomodoro_edit|error:insufficient_permissions|role_needed:admin" msgid "You need to be a guild admin to modify this option!" msgstr "" -#: src/modules/pomodoro/cog.py:850 +#: src/modules/pomodoro/cog.py:865 msgctxt "cmd:pomodoro_edit|error:insufficient_permissions|role_needed:owner" msgid "You need to be a channel owner or guild admin to modify this option!" msgstr "" -#: src/modules/pomodoro/cog.py:855 +#: src/modules/pomodoro/cog.py:870 msgctxt "cmd:pomodoro_edit|error:insufficient_permissions|role_needed:manager" msgid "" "You need to be a guild admin or have the manager role to modify this option!" msgstr "" -#: src/modules/pomodoro/cog.py:891 +#: src/modules/pomodoro/cog.py:906 msgctxt "cmd:configure_pomodoro" msgid "pomodoro" msgstr "" -#: src/modules/pomodoro/cog.py:892 +#: src/modules/pomodoro/cog.py:907 msgctxt "cmd:configure_pomodoro|desc" msgid "Configure Pomodoro Timer System" msgstr "" diff --git a/locales/templates/babel.pot b/locales/templates/babel.pot index 43a4ca71..4449941c 100644 --- a/locales/templates/babel.pot +++ b/locales/templates/babel.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -84,7 +84,7 @@ msgctxt "cmd:userconfig_language|button:reset|label" msgid "Reset" msgstr "" -#: src/babel/cog.py:251 +#: src/babel/cog.py:252 #, possible-python-brace-format msgctxt "acmpl:language|no_match" msgid "No supported languages matching {partial}" @@ -219,162 +219,172 @@ msgctxt "guildset:locale|response" msgid "You have set the guild language to {lang}." msgstr "" -#: src/babel/enums.py:42 -msgctxt "localenames|locale:en-US" -msgid "American English" -msgstr "" - #: src/babel/enums.py:43 -msgctxt "localenames|locale:en-GB" -msgid "British English" +msgctxt "localenames|locale:id" +msgid "Indonesian" msgstr "" #: src/babel/enums.py:44 -msgctxt "localenames|locale:bg" -msgid "Bulgarian" -msgstr "" - -#: src/babel/enums.py:45 -msgctxt "localenames|locale:zh-CN" -msgid "Chinese" -msgstr "" - -#: src/babel/enums.py:46 -msgctxt "localenames|locale:zh-TW" -msgid "Taiwan Chinese" -msgstr "" - -#: src/babel/enums.py:47 -msgctxt "localenames|locale:hr" -msgid "Croatian" -msgstr "" - -#: src/babel/enums.py:48 -msgctxt "localenames|locale:cs" -msgid "Czech" -msgstr "" - -#: src/babel/enums.py:49 msgctxt "localenames|locale:da" msgid "Danish" msgstr "" -#: src/babel/enums.py:50 -msgctxt "localenames|locale:nl" -msgid "Dutch" -msgstr "" - -#: src/babel/enums.py:51 -msgctxt "localenames|locale:fi" -msgid "Finnish" -msgstr "" - -#: src/babel/enums.py:52 -msgctxt "localenames|locale:fr" -msgid "French" -msgstr "" - -#: src/babel/enums.py:53 +#: src/babel/enums.py:45 msgctxt "localenames|locale:de" msgid "German" msgstr "" -#: src/babel/enums.py:54 -msgctxt "localenames|locale:el" -msgid "Greek" +#: src/babel/enums.py:46 +msgctxt "localenames|locale:en-GB" +msgid "English, UK" msgstr "" -#: src/babel/enums.py:55 -msgctxt "localenames|locale:hi" -msgid "Hindi" +#: src/babel/enums.py:47 +msgctxt "localenames|locale:en-US" +msgid "English, US" msgstr "" -#: src/babel/enums.py:56 -msgctxt "localenames|locale:hu" -msgid "Hungarian" +#: src/babel/enums.py:48 +msgctxt "localenames|locale:es-ES" +msgid "Spanish" msgstr "" -#: src/babel/enums.py:57 +#: src/babel/enums.py:49 +msgctxt "localenames|locale:fr" +msgid "French" +msgstr "" + +#: src/babel/enums.py:50 +msgctxt "localenames|locale:hr" +msgid "Croatian" +msgstr "" + +#: src/babel/enums.py:51 msgctxt "localenames|locale:it" msgid "Italian" msgstr "" -#: src/babel/enums.py:58 -msgctxt "localenames|locale:ja" -msgid "Japanese" -msgstr "" - -#: src/babel/enums.py:59 -msgctxt "localenames|locale:ko" -msgid "Korean" -msgstr "" - -#: src/babel/enums.py:60 +#: src/babel/enums.py:52 msgctxt "localenames|locale:lt" msgid "Lithuanian" msgstr "" -#: src/babel/enums.py:61 +#: src/babel/enums.py:53 +msgctxt "localenames|locale:hu" +msgid "Hungarian" +msgstr "" + +#: src/babel/enums.py:54 +msgctxt "localenames|locale:nl" +msgid "Dutch" +msgstr "" + +#: src/babel/enums.py:55 msgctxt "localenames|locale:no" msgid "Norwegian" msgstr "" -#: src/babel/enums.py:62 +#: src/babel/enums.py:56 msgctxt "localenames|locale:pl" msgid "Polish" msgstr "" -#: src/babel/enums.py:63 +#: src/babel/enums.py:57 msgctxt "localenames|locale:pt-BR" -msgid "Brazil Portuguese" +msgid "Portuguese, Brazilian" msgstr "" -#: src/babel/enums.py:64 +#: src/babel/enums.py:58 msgctxt "localenames|locale:ro" -msgid "Romanian" +msgid "Romanian, Romania" msgstr "" -#: src/babel/enums.py:65 -msgctxt "localenames|locale:ru" -msgid "Russian" +#: src/babel/enums.py:59 +msgctxt "localenames|locale:fi" +msgid "Finnish" msgstr "" -#: src/babel/enums.py:66 -msgctxt "localenames|locale:es-ES" -msgid "Spain Spanish" -msgstr "" - -#: src/babel/enums.py:67 +#: src/babel/enums.py:60 msgctxt "localenames|locale:sv-SE" msgid "Swedish" msgstr "" -#: src/babel/enums.py:68 -msgctxt "localenames|locale:th" -msgid "Thai" -msgstr "" - -#: src/babel/enums.py:69 -msgctxt "localenames|locale:tr" -msgid "Turkish" -msgstr "" - -#: src/babel/enums.py:70 -msgctxt "localenames|locale:uk" -msgid "Ukrainian" -msgstr "" - -#: src/babel/enums.py:71 +#: src/babel/enums.py:61 msgctxt "localenames|locale:vi" msgid "Vietnamese" msgstr "" +#: src/babel/enums.py:62 +msgctxt "localenames|locale:tr" +msgid "Turkish" +msgstr "" + +#: src/babel/enums.py:63 +msgctxt "localenames|locale:cs" +msgid "Czech" +msgstr "" + +#: src/babel/enums.py:64 +msgctxt "localenames|locale:el" +msgid "Greek" +msgstr "" + +#: src/babel/enums.py:65 +msgctxt "localenames|locale:bg" +msgid "Bulgarian" +msgstr "" + +#: src/babel/enums.py:66 +msgctxt "localenames|locale:ru" +msgid "Russian" +msgstr "" + +#: src/babel/enums.py:67 +msgctxt "localenames|locale:uk" +msgid "Ukrainian" +msgstr "" + +#: src/babel/enums.py:68 +msgctxt "localenames|locale:hi" +msgid "Hindi" +msgstr "" + +#: src/babel/enums.py:69 +msgctxt "localenames|locale:th" +msgid "Thai" +msgstr "" + +#: src/babel/enums.py:70 +msgctxt "localenames|locale:zh-CN" +msgid "Chinese, China" +msgstr "" + +#: src/babel/enums.py:71 +msgctxt "localenames|locale:ja" +msgid "Japanese" +msgstr "" + #: src/babel/enums.py:72 +msgctxt "localenames|locale:zh-TW" +msgid "Chinese, Taiwan" +msgstr "" + +#: src/babel/enums.py:73 +msgctxt "localenames|locale:ko" +msgid "Korean" +msgstr "" + +#: src/babel/enums.py:78 msgctxt "localenames|locale:he" msgid "Hebrew" msgstr "" -#: src/babel/enums.py:73 -msgctxt "localenames|locale:he_IL" -msgid "Hebrew (Israel)" +#: src/babel/enums.py:79 +msgctxt "localenames|locale:he-IL" +msgid "Hebrew" +msgstr "" + +#: src/babel/enums.py:80 +msgctxt "localenames|locale:test" +msgid "Test Language" msgstr "" diff --git a/locales/templates/config.pot b/locales/templates/config.pot index cc464ce4..b0fd303f 100644 --- a/locales/templates/config.pot +++ b/locales/templates/config.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/locales/templates/core_config.pot b/locales/templates/core_config.pot index 4c514961..eb0e75bd 100644 --- a/locales/templates/core_config.pot +++ b/locales/templates/core_config.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/locales/templates/economy.pot b/locales/templates/economy.pot index f1459b45..674af36d 100644 --- a/locales/templates/economy.pot +++ b/locales/templates/economy.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/locales/templates/exec.pot b/locales/templates/exec.pot index 5eefe627..02294f32 100644 --- a/locales/templates/exec.pot +++ b/locales/templates/exec.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,74 +17,74 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: src/modules/sysadmin/exec_cog.py:257 +#: src/modules/sysadmin/exec_cog.py:269 msgctxt "ward:sys_admin|failed" msgid "You must be a bot owner to do this!" msgstr "" -#: src/modules/sysadmin/exec_cog.py:262 +#: src/modules/sysadmin/exec_cog.py:274 msgid "async" msgstr "" -#: src/modules/sysadmin/exec_cog.py:263 +#: src/modules/sysadmin/exec_cog.py:275 msgid "Execute arbitrary code with Exec" msgstr "" -#: src/modules/sysadmin/exec_cog.py:325 +#: src/modules/sysadmin/exec_cog.py:337 msgctxt "command" msgid "eval" msgstr "" -#: src/modules/sysadmin/exec_cog.py:326 +#: src/modules/sysadmin/exec_cog.py:338 msgctxt "command:eval" msgid "Execute arbitrary code with Eval" msgstr "" -#: src/modules/sysadmin/exec_cog.py:329 +#: src/modules/sysadmin/exec_cog.py:341 msgctxt "command:eval|param:string" msgid "Code to evaluate." msgstr "" -#: src/modules/sysadmin/exec_cog.py:336 +#: src/modules/sysadmin/exec_cog.py:348 msgctxt "command" msgid "asyncall" msgstr "" -#: src/modules/sysadmin/exec_cog.py:337 +#: src/modules/sysadmin/exec_cog.py:349 msgctxt "command:asyncall|desc" msgid "Execute arbitrary code on all shards." msgstr "" -#: src/modules/sysadmin/exec_cog.py:340 +#: src/modules/sysadmin/exec_cog.py:352 msgctxt "command:asyncall|param:string" msgid "Cross-shard code to execute. Cannot reference ctx!" msgstr "" -#: src/modules/sysadmin/exec_cog.py:341 +#: src/modules/sysadmin/exec_cog.py:353 msgctxt "command:asyncall|param:target" msgid "Target shard app name, see autocomplete for options." msgstr "" -#: src/modules/sysadmin/exec_cog.py:384 +#: src/modules/sysadmin/exec_cog.py:396 msgid "reload" msgstr "" -#: src/modules/sysadmin/exec_cog.py:385 +#: src/modules/sysadmin/exec_cog.py:397 msgid "Reload a given LionBot extension. Launches an ExecUI." msgstr "" -#: src/modules/sysadmin/exec_cog.py:388 +#: src/modules/sysadmin/exec_cog.py:400 msgid "Name of the extension to reload. See autocomplete for options." msgstr "" -#: src/modules/sysadmin/exec_cog.py:389 +#: src/modules/sysadmin/exec_cog.py:401 msgid "Whether to force an extension reload even if it doesn't exist." msgstr "" -#: src/modules/sysadmin/exec_cog.py:425 +#: src/modules/sysadmin/exec_cog.py:437 msgid "shutdown" msgstr "" -#: src/modules/sysadmin/exec_cog.py:426 +#: src/modules/sysadmin/exec_cog.py:438 msgid "Shutdown (or restart) the client." msgstr "" diff --git a/locales/templates/goals-gui.pot b/locales/templates/goals-gui.pot index ebd4186b..ada822f5 100644 --- a/locales/templates/goals-gui.pot +++ b/locales/templates/goals-gui.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/locales/templates/leaderboard-gui.pot b/locales/templates/leaderboard-gui.pot index d9de564a..8b74d1c5 100644 --- a/locales/templates/leaderboard-gui.pot +++ b/locales/templates/leaderboard-gui.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/locales/templates/lion-core.pot b/locales/templates/lion-core.pot index 2677acac..9a51cee0 100644 --- a/locales/templates/lion-core.pot +++ b/locales/templates/lion-core.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -27,45 +27,47 @@ msgctxt "settype:coin|parse|error:notinteger" msgid "The coin quantity must be a positive integer!" msgstr "" -#: src/core/setting_types.py:54 +#: src/core/setting_types.py:55 +#, possible-python-brace-format msgctxt "settype:coin|parse|error:too_large" -msgid "Provided number of coins was too high!" +msgid "You cannot set this to more than {coin}**{max}**!" msgstr "" -#: src/core/setting_types.py:60 -msgctxt "settype:coin|parse|error:too_large" -msgid "Provided number of coins was too low!" +#: src/core/setting_types.py:63 +#, possible-python-brace-format +msgctxt "settype:coin|parse|error:too_small" +msgid "You cannot set this to less than {coin}**{min}**!" msgstr "" -#: src/core/setting_types.py:71 +#: src/core/setting_types.py:75 #, possible-python-brace-format msgctxt "settype:coin|formatted" msgid "{coin}**{amount}**" msgstr "" -#: src/core/setting_types.py:87 +#: src/core/setting_types.py:91 msgctxt "settype:message|accepts" msgid "JSON formatted raw message data" msgstr "" -#: src/core/setting_types.py:102 +#: src/core/setting_types.py:106 msgctxt "settype:message|download|error:not_json" msgid "The attached message data is not a JSON file!" msgstr "" -#: src/core/setting_types.py:107 +#: src/core/setting_types.py:111 msgctxt "settype:message|download|error:size" msgid "The attached message data is too large!" msgstr "" -#: src/core/setting_types.py:116 +#: src/core/setting_types.py:120 msgctxt "settype:message|download|error:decoding" msgid "" "Could not decode the message data. Please ensure it is saved with the " "`UTF-8` encoding." msgstr "" -#: src/core/setting_types.py:173 +#: src/core/setting_types.py:177 #, possible-python-brace-format msgctxt "settype:message|error_suffix" msgid "" @@ -73,7 +75,7 @@ msgid "" "({link})." msgstr "" -#: src/core/setting_types.py:185 +#: src/core/setting_types.py:189 #, possible-python-brace-format msgctxt "settype:message|error:invalid_json" msgid "" @@ -81,34 +83,34 @@ msgid "" "`{error}`" msgstr "" -#: src/core/setting_types.py:193 +#: src/core/setting_types.py:197 msgctxt "settype:message|error:json_missing_keys" msgid "" "Message data must be a JSON object with at least one of the following " "fields: `content`, `embed`, `embeds`" msgstr "" -#: src/core/setting_types.py:202 +#: src/core/setting_types.py:206 msgctxt "settype:message|error:json_embed_type" msgid "`embed` field must be a valid JSON object." msgstr "" -#: src/core/setting_types.py:210 +#: src/core/setting_types.py:214 msgctxt "settype:message|error:json_embeds_type" msgid "`embeds` field must be a list." msgstr "" -#: src/core/setting_types.py:217 +#: src/core/setting_types.py:221 msgctxt "settype:message|error:json_embed_embeds" msgid "Message data cannot include both `embed` and `embeds`." msgstr "" -#: src/core/setting_types.py:225 +#: src/core/setting_types.py:229 msgctxt "settype:message|error:json_content_type" msgid "`content` field must be a string." msgstr "" -#: src/core/setting_types.py:241 +#: src/core/setting_types.py:245 #, possible-python-brace-format msgctxt "ui:settype:message|error:embed_conversion" msgid "" @@ -116,7 +118,7 @@ msgid "" "**Error:** `{exception}`" msgstr "" -#: src/core/setting_types.py:269 +#: src/core/setting_types.py:273 msgctxt "settype:message|format:too_long" msgid "Too long to display! See Preview." msgstr "" diff --git a/locales/templates/member_admin.pot b/locales/templates/member_admin.pot index 9868296e..7d4c7857 100644 --- a/locales/templates/member_admin.pot +++ b/locales/templates/member_admin.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -44,7 +44,7 @@ msgstr "" #: src/modules/member_admin/settingui.py:213 msgctxt "ui:memberadmin|embed|title" -msgid "Member Admin Configuration Panel" +msgid "Greetings and Initial Roles Panel" msgstr "" #: src/modules/member_admin/settingui.py:257 @@ -67,39 +67,39 @@ msgctxt "dash:member_admin|section:initial_roles|name" msgid "Initial Roles ({commands[configure welcome]})" msgstr "" -#: src/modules/member_admin/cog.py:234 +#: src/modules/member_admin/cog.py:239 msgctxt "cmd:resetmember" msgid "resetmember" msgstr "" -#: src/modules/member_admin/cog.py:237 +#: src/modules/member_admin/cog.py:242 msgctxt "cmd:resetmember|desc" msgid "Reset (server-associated) member data for the target member or user." msgstr "" -#: src/modules/member_admin/cog.py:241 +#: src/modules/member_admin/cog.py:246 msgctxt "cmd:resetmember|param:target" msgid "member_to_reset" msgstr "" -#: src/modules/member_admin/cog.py:242 +#: src/modules/member_admin/cog.py:247 msgctxt "cmd:resetmember|param:saved_roles" msgid "saved_roles" msgstr "" -#: src/modules/member_admin/cog.py:247 +#: src/modules/member_admin/cog.py:252 msgctxt "cmd:resetmember|param:target|desc" msgid "Choose the member (current or past) you want to reset." msgstr "" -#: src/modules/member_admin/cog.py:251 +#: src/modules/member_admin/cog.py:256 msgctxt "cmd:resetmember|param:saved_roles|desc" msgid "" "Clear the saved roles for this member, so their past roles are not restored " "on rejoin." msgstr "" -#: src/modules/member_admin/cog.py:278 +#: src/modules/member_admin/cog.py:283 #, possible-python-brace-format msgctxt "cmd:resetmember|reset:saved_roles|success" msgid "" @@ -107,17 +107,17 @@ msgid "" "roles if they rejoin." msgstr "" -#: src/modules/member_admin/cog.py:286 +#: src/modules/member_admin/cog.py:291 msgctxt "cmd:resetmember|error:nothing_to_do" msgid "No reset operation selected, nothing to do." msgstr "" -#: src/modules/member_admin/cog.py:302 +#: src/modules/member_admin/cog.py:307 msgctxt "cmd:configure_welcome" msgid "welcome" msgstr "" -#: src/modules/member_admin/cog.py:305 +#: src/modules/member_admin/cog.py:310 msgctxt "cmd:configure_welcome|desc" msgid "Configure new member greetings and roles." msgstr "" diff --git a/locales/templates/meta.pot b/locales/templates/meta.pot index 77687e92..d76b7657 100644 --- a/locales/templates/meta.pot +++ b/locales/templates/meta.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/locales/templates/moderation.pot b/locales/templates/moderation.pot index 609b0bc6..25344d32 100644 --- a/locales/templates/moderation.pot +++ b/locales/templates/moderation.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/locales/templates/monthly-gui.pot b/locales/templates/monthly-gui.pot index a7006170..73997045 100644 --- a/locales/templates/monthly-gui.pot +++ b/locales/templates/monthly-gui.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/locales/templates/profile-gui.pot b/locales/templates/profile-gui.pot index 0a3fbac4..7767010f 100644 --- a/locales/templates/profile-gui.pot +++ b/locales/templates/profile-gui.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/locales/templates/ranks.pot b/locales/templates/ranks.pot index 6f0fcf33..141124e5 100644 --- a/locales/templates/ranks.pot +++ b/locales/templates/ranks.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -32,17 +32,22 @@ msgctxt "cmd:configure_ranks|param:rank_type|choice:message" msgid "Message" msgstr "" -#: src/modules/ranks/cog.py:406 +#: src/modules/ranks/cog.py:495 msgctxt "event:rank_update|embed:notify" msgid "New Activity Rank Attained!" msgstr "" -#: src/modules/ranks/cog.py:516 +#: src/modules/ranks/cog.py:601 +msgctxt "rank_refresh|error:cannot_chunk|desc" +msgid "Could not retrieve member list from Discord. Please try again later." +msgstr "" + +#: src/modules/ranks/cog.py:614 msgctxt "rank_refresh|error:roles_dne|desc" msgid "Some ranks have invalid or deleted roles! Please remove them first." msgstr "" -#: src/modules/ranks/cog.py:526 +#: src/modules/ranks/cog.py:624 #, possible-python-brace-format msgctxt "rank_refresh|error:unassignable_roles|desc" msgid "" @@ -50,36 +55,36 @@ msgid "" "{roles}" msgstr "" -#: src/modules/ranks/cog.py:596 +#: src/modules/ranks/cog.py:694 msgctxt "rank_refresh|remove_roles|audit" msgid "Removing invalid rank role." msgstr "" -#: src/modules/ranks/cog.py:610 +#: src/modules/ranks/cog.py:708 #, possible-python-brace-format msgctxt "rank_refresh|remove_roles|small_error" msgid "*Could not remove ranks from {member}*" msgstr "" -#: src/modules/ranks/cog.py:617 +#: src/modules/ranks/cog.py:715 msgctxt "rank_refresh|remove_roles|error:too_many_issues" msgid "" "Too many issues occurred while removing ranks! Please check my permissions " "and try again in a few minutes." msgstr "" -#: src/modules/ranks/cog.py:631 +#: src/modules/ranks/cog.py:729 msgctxt "rank_refresh|add_roles|audit" msgid "Adding rank role from refresh" msgstr "" -#: src/modules/ranks/cog.py:645 +#: src/modules/ranks/cog.py:743 #, possible-python-brace-format msgctxt "rank_refresh|add_roles|small_error" msgid "*Could not add {role} to {member}*" msgstr "" -#: src/modules/ranks/cog.py:652 +#: src/modules/ranks/cog.py:750 msgctxt "rank_refresh|add_roles|error:too_many_issues" msgid "" "Too many issues occurred while adding ranks! Please check my permissions and " @@ -87,22 +92,22 @@ msgid "" msgstr "" #. ---------- Commands ---------- -#: src/modules/ranks/cog.py:677 +#: src/modules/ranks/cog.py:775 msgctxt "cmd:ranks" msgid "ranks" msgstr "" -#: src/modules/ranks/cog.py:709 +#: src/modules/ranks/cog.py:807 msgctxt "cmd:configure_ranks" msgid "ranks" msgstr "" -#: src/modules/ranks/cog.py:710 +#: src/modules/ranks/cog.py:808 msgctxt "cmd:configure_ranks|desc" msgid "Configure Activity Ranks" msgstr "" -#: src/modules/ranks/cog.py:770 +#: src/modules/ranks/cog.py:868 #, possible-python-brace-format msgctxt "" "cmd:configure_ranks|response:updated|setting:notification|withdm_withchannel" @@ -111,20 +116,20 @@ msgid "" "otherwise to {channel}" msgstr "" -#: src/modules/ranks/cog.py:776 +#: src/modules/ranks/cog.py:874 msgctxt "" "cmd:configure_ranks|response:updated|setting:notification|withdm_nochannel" msgid "Rank update notifications will be sent via **direct message**." msgstr "" -#: src/modules/ranks/cog.py:782 +#: src/modules/ranks/cog.py:880 #, possible-python-brace-format msgctxt "" "cmd:configure_ranks|response:updated|setting:notification|nodm_withchannel" msgid "Rank update notifications will be sent to {channel}." msgstr "" -#: src/modules/ranks/cog.py:787 +#: src/modules/ranks/cog.py:885 msgctxt "" "cmd:configure_ranks|response:updated|setting:notification|nodm_nochannel" msgid "Members will not be notified when their activity rank updates." @@ -346,31 +351,31 @@ msgctxt "guildset:dm_ranks|response:false" msgid "I will never direct message members upon rank advancement." msgstr "" -#: src/modules/ranks/ui/preview.py:74 +#: src/modules/ranks/ui/preview.py:75 msgctxt "ui:rank_preview|button:edit|error:role_deleted" msgid "" "The role underlying this rank no longer exists! Please select a new role " "from the role menu." msgstr "" -#: src/modules/ranks/ui/preview.py:81 +#: src/modules/ranks/ui/preview.py:82 msgctxt "ui:rank_preview|button:edit|error:role_not_assignable" msgid "" "I do not have permission to edit the underlying role! Please select a new " "role from the role menu, or ensure my top role is above the selected role." msgstr "" -#: src/modules/ranks/ui/preview.py:90 +#: src/modules/ranks/ui/preview.py:91 msgctxt "ui:rank_preview|button:edit|error|title" msgid "Failed to edit rank!" msgstr "" -#: src/modules/ranks/ui/preview.py:108 +#: src/modules/ranks/ui/preview.py:109 msgctxt "ui:rank_preview|button:edit|label" msgid "Edit" msgstr "" -#: src/modules/ranks/ui/preview.py:139 +#: src/modules/ranks/ui/preview.py:142 #, possible-python-brace-format msgctxt "ui:rank_preview|button:delete|response:success|description|with_role" msgid "" @@ -378,24 +383,24 @@ msgid "" "the role." msgstr "" -#: src/modules/ranks/ui/preview.py:144 +#: src/modules/ranks/ui/preview.py:147 #, possible-python-brace-format msgctxt "ui:rank_preview|button:delete|response:success|description|no_role" msgid "You have deleted the rank {mention}." msgstr "" -#: src/modules/ranks/ui/preview.py:150 +#: src/modules/ranks/ui/preview.py:153 msgctxt "ui:rank_preview|button:delete|response:success|title" msgid "Rank Deleted" msgstr "" -#: src/modules/ranks/ui/preview.py:160 +#: src/modules/ranks/ui/preview.py:163 msgctxt "" "ui:rank_preview|button:delete|response:success|button:delete_role|label" msgid "Delete Role" msgstr "" -#: src/modules/ranks/ui/preview.py:176 +#: src/modules/ranks/ui/preview.py:179 #, possible-python-brace-format msgctxt "" "ui:rank_preview|button:delete|response:success|button:delete_role|response:" @@ -405,7 +410,7 @@ msgid "" "unknown error." msgstr "" -#: src/modules/ranks/ui/preview.py:182 +#: src/modules/ranks/ui/preview.py:185 #, possible-python-brace-format msgctxt "" "ui:rank_preview|button:delete|response:success|button:delete_role|response:" @@ -413,37 +418,24 @@ msgctxt "" msgid "You have deleted the rank **{name}** along with the underlying role." msgstr "" -#: src/modules/ranks/ui/preview.py:199 +#: src/modules/ranks/ui/preview.py:202 msgctxt "ui:rank_preview|button:delete|label" msgid "Delete Rank" msgstr "" -#: src/modules/ranks/ui/preview.py:219 -#, possible-python-brace-format -msgctxt "ui:rank_preview|menu:roles|error:above_caller" -msgid "" -"You have insufficient permissions to assign {mention} as a rank role! You " -"may only manage roles below your top role." -msgstr "" - -#: src/modules/ranks/ui/preview.py:225 -msgctxt "ui:rank_preview|menu:roles|error:above_caller|title" -msgid "Insufficient permissions!" -msgstr "" - -#: src/modules/ranks/ui/preview.py:241 +#: src/modules/ranks/ui/preview.py:232 msgctxt "ui:rank_preview|menu:roles|error:not_assignable|suberror:is_default" msgid "The @everyone role cannot be removed, and cannot be a rank!" msgstr "" -#: src/modules/ranks/ui/preview.py:246 +#: src/modules/ranks/ui/preview.py:237 msgctxt "ui:rank_preview|menu:roles|error:not_assignable|suberror:is_managed" msgid "" "The role is managed by another application or integration, and cannot be a " "rank!" msgstr "" -#: src/modules/ranks/ui/preview.py:251 +#: src/modules/ranks/ui/preview.py:242 msgctxt "" "ui:rank_preview|menu:roles|error:not_assignable|suberror:no_permissions" msgid "" @@ -451,121 +443,121 @@ msgid "" "manage ranks!" msgstr "" -#: src/modules/ranks/ui/preview.py:256 +#: src/modules/ranks/ui/preview.py:247 msgctxt "ui:rank_preview|menu:roles|error:not_assignable|suberror:above_me" msgid "" "This role is above my top role in the role hierarchy, so I cannot add or " "remove it!" msgstr "" -#: src/modules/ranks/ui/preview.py:262 +#: src/modules/ranks/ui/preview.py:253 msgctxt "ui:rank_preview|menu:roles|error:not_assignable|suberror:other" msgid "I am not able to manage the selected role, so it cannot be a rank!" msgstr "" -#: src/modules/ranks/ui/preview.py:268 +#: src/modules/ranks/ui/preview.py:259 msgctxt "ui:rank_preview|menu:roles|error:not_assignable|title" msgid "Could not update rank!" msgstr "" -#: src/modules/ranks/ui/preview.py:278 +#: src/modules/ranks/ui/preview.py:269 msgctxt "ui:rank_preview|menu:roles|placeholder" msgid "Update Rank Role" msgstr "" -#: src/modules/ranks/ui/preview.py:290 +#: src/modules/ranks/ui/preview.py:281 msgctxt "ui:rank_preview|embed|title" msgid "Rank Information" msgstr "" -#: src/modules/ranks/ui/preview.py:297 +#: src/modules/ranks/ui/preview.py:288 msgctxt "ui:rank_preview|embed|field:role|name" msgid "Role" msgstr "" -#: src/modules/ranks/ui/preview.py:304 +#: src/modules/ranks/ui/preview.py:295 msgctxt "ui:rank_preview|embed|field:required|name" msgid "Required" msgstr "" -#: src/modules/ranks/ui/preview.py:311 +#: src/modules/ranks/ui/preview.py:302 msgctxt "ui:rank_preview|embed|field:reward|name" msgid "Reward" msgstr "" -#: src/modules/ranks/ui/preview.py:320 +#: src/modules/ranks/ui/preview.py:311 msgctxt "ui:rank_preview|embed|field:message" msgid "Congratulatory Message" msgstr "" -#: src/modules/ranks/ui/refresh.py:125 +#: src/modules/ranks/ui/refresh.py:134 msgctxt "ui:refresh_ranks|embed|title:errored" msgid "Could not refresh the server ranks!" msgstr "" -#: src/modules/ranks/ui/refresh.py:133 +#: src/modules/ranks/ui/refresh.py:142 msgctxt "ui:refresh_ranks|embed|title:done" msgid "Rank refresh complete!" msgstr "" -#: src/modules/ranks/ui/refresh.py:139 +#: src/modules/ranks/ui/refresh.py:148 msgctxt "ui:refresh_ranks|embed|title:working" msgid "Refreshing your server ranks, please wait." msgstr "" -#: src/modules/ranks/ui/refresh.py:157 +#: src/modules/ranks/ui/refresh.py:166 #, possible-python-brace-format msgctxt "ui:refresh_ranks|embed|line:ranks" msgid "**Loading server ranks:** {emoji}" msgstr "" -#: src/modules/ranks/ui/refresh.py:167 +#: src/modules/ranks/ui/refresh.py:176 #, possible-python-brace-format msgctxt "ui:refresh_ranks|embed|line:members" msgid "**Loading server members:** {emoji}" msgstr "" -#: src/modules/ranks/ui/refresh.py:177 +#: src/modules/ranks/ui/refresh.py:186 #, possible-python-brace-format msgctxt "ui:refresh_ranks|embed|line:roles" msgid "**Loading rank roles:** {emoji}" msgstr "" -#: src/modules/ranks/ui/refresh.py:187 +#: src/modules/ranks/ui/refresh.py:196 #, possible-python-brace-format msgctxt "ui:refresh_ranks|embed|line:compute" msgid "**Computing correct ranks:** {emoji}" msgstr "" -#: src/modules/ranks/ui/refresh.py:198 +#: src/modules/ranks/ui/refresh.py:207 msgctxt "ui:refresh_ranks|embed|field:remove|name" msgid "Removing invalid rank roles from members" msgstr "" -#: src/modules/ranks/ui/refresh.py:202 +#: src/modules/ranks/ui/refresh.py:211 #, possible-python-brace-format msgctxt "ui:refresh_ranks|embed|field:remove|value" msgid "{progress} {done}/{total} removed" msgstr "" -#: src/modules/ranks/ui/refresh.py:213 +#: src/modules/ranks/ui/refresh.py:222 #, possible-python-brace-format msgctxt "ui:refresh_ranks|embed|line:remove" msgid "**Removed invalid ranks:** {done}/{target}" msgstr "" -#: src/modules/ranks/ui/refresh.py:221 +#: src/modules/ranks/ui/refresh.py:230 msgctxt "ui:refresh_ranks|embed|field:add|name" msgid "Giving members their rank roles" msgstr "" -#: src/modules/ranks/ui/refresh.py:225 +#: src/modules/ranks/ui/refresh.py:234 #, possible-python-brace-format msgctxt "ui:refresh_ranks|embed|field:add|value" msgid "{progress} {done}/{total} given" msgstr "" -#: src/modules/ranks/ui/refresh.py:236 +#: src/modules/ranks/ui/refresh.py:245 #, possible-python-brace-format msgctxt "ui:refresh_ranks|embed|line:add" msgid "**Updated member ranks:** {done}/{target}" @@ -621,67 +613,54 @@ msgctxt "dash:rank|dropdown|placeholder" msgid "Activity Rank Panel" msgstr "" -#: src/modules/ranks/ui/overview.py:94 +#: src/modules/ranks/ui/overview.py:95 msgctxt "ui:rank_overview|button:auto|label" msgid "Auto Create" msgstr "" -#: src/modules/ranks/ui/overview.py:109 +#: src/modules/ranks/ui/overview.py:110 msgctxt "ui:rank_overview|button:refresh|label" msgid "Refresh Member Ranks" msgstr "" -#: src/modules/ranks/ui/overview.py:121 +#: src/modules/ranks/ui/overview.py:122 msgctxt "ui:rank_overview|button:clear|confirm" msgid "Are you sure you want to **delete all activity ranks** in this server?" msgstr "" -#: src/modules/ranks/ui/overview.py:126 +#: src/modules/ranks/ui/overview.py:127 msgctxt "ui:rank_overview|button:clear|confirm|button:yes" msgid "Yes, clear ranks" msgstr "" -#: src/modules/ranks/ui/overview.py:132 +#: src/modules/ranks/ui/overview.py:133 msgctxt "ui:rank_overview|button:clear|confirm|button:no" msgid "Cancel" msgstr "" -#: src/modules/ranks/ui/overview.py:148 +#: src/modules/ranks/ui/overview.py:149 msgctxt "ui:rank_overview|button:clear|label" msgid "Clear Ranks" msgstr "" -#: src/modules/ranks/ui/overview.py:178 +#: src/modules/ranks/ui/overview.py:179 msgctxt "ui:rank_overview|button:create|label" msgid "Create Rank" msgstr "" -#: src/modules/ranks/ui/overview.py:194 -#, possible-python-brace-format -msgctxt "ui:rank_overview|menu:roles|error:above_caller" -msgid "" -"You have insufficient permissions to assign {mention} as a rank role! You " -"may only manage roles below your top role." -msgstr "" - -#: src/modules/ranks/ui/overview.py:200 -msgctxt "ui:rank_overview|menu:roles|error:above_caller|title" -msgid "Insufficient permissions!" -msgstr "" - -#: src/modules/ranks/ui/overview.py:233 +#: src/modules/ranks/ui/overview.py:222 msgctxt "ui:rank_overview|menu:roles|error:not_assignable|suberror:is_default" msgid "The @everyone role cannot be removed, and cannot be a rank!" msgstr "" -#: src/modules/ranks/ui/overview.py:238 +#: src/modules/ranks/ui/overview.py:227 msgctxt "ui:rank_overview|menu:roles|error:not_assignable|suberror:is_managed" msgid "" "The role is managed by another application or integration, and cannot be a " "rank!" msgstr "" -#: src/modules/ranks/ui/overview.py:243 +#: src/modules/ranks/ui/overview.py:232 msgctxt "" "ui:rank_overview|menu:roles|error:not_assignable|suberror:no_permissions" msgid "" @@ -689,45 +668,71 @@ msgid "" "manage ranks!" msgstr "" -#: src/modules/ranks/ui/overview.py:248 +#: src/modules/ranks/ui/overview.py:237 msgctxt "ui:rank_overview|menu:roles|error:not_assignable|suberror:above_me" msgid "" "This role is above my top role in the role hierarchy, so I cannot add or " "remove it!" msgstr "" -#: src/modules/ranks/ui/overview.py:254 +#: src/modules/ranks/ui/overview.py:243 msgctxt "ui:rank_overview|menu:roles|error:not_assignable|suberror:other" msgid "I am not able to manage the selected role, so it cannot be a rank!" msgstr "" -#: src/modules/ranks/ui/overview.py:260 +#: src/modules/ranks/ui/overview.py:249 msgctxt "ui:rank_overview|menu:roles|error:not_assignable|title" msgid "Could not create rank!" msgstr "" -#: src/modules/ranks/ui/overview.py:284 +#: src/modules/ranks/ui/overview.py:273 msgctxt "ui:rank_overview|menu:roles|placeholder" msgid "Create from role" msgstr "" -#: src/modules/ranks/ui/overview.py:301 +#: src/modules/ranks/ui/overview.py:290 msgctxt "ui:rank_overview|menu:ranks|placeholder" msgid "View or edit rank" msgstr "" -#: src/modules/ranks/ui/overview.py:387 +#: src/modules/ranks/ui/overview.py:376 +msgctxt "ui:rank_overview|embed:noranks|desc" +msgid "" +"No activity ranks have been set up!\n" +"Press 'AUTO' to automatically create a standard heirachy of voice | text | " +"xp ranks, or select a role or press Create below!" +msgstr "" + +#: src/modules/ranks/ui/overview.py:384 +#, possible-python-brace-format +msgctxt "ui:rank_overview|embed|title|type:voice" +msgid "Voice Ranks in {guild_name}" +msgstr "" + +#: src/modules/ranks/ui/overview.py:389 +#, possible-python-brace-format +msgctxt "ui:rank_overview|embed|title|type:xp" +msgid "XP ranks in {guild_name}" +msgstr "" + +#: src/modules/ranks/ui/overview.py:394 +#, possible-python-brace-format +msgctxt "ui:rank_overview|embed|title|type:message" +msgid "Message ranks in {guild_name}" +msgstr "" + +#: src/modules/ranks/ui/overview.py:406 msgctxt "ui:rank_overview|embed|field:note|name" msgid "Note" msgstr "" -#: src/modules/ranks/ui/overview.py:393 +#: src/modules/ranks/ui/overview.py:412 #, possible-python-brace-format msgctxt "ui:rank_overview|embed|field:note|value:with_season" msgid "Ranks are determined by activity since {timestamp}." msgstr "" -#: src/modules/ranks/ui/overview.py:400 +#: src/modules/ranks/ui/overview.py:419 #, possible-python-brace-format msgctxt "ui:rank_overview|embed|field:note|value:without_season" msgid "" @@ -736,7 +741,7 @@ msgid "" "ranks) set the `season_start` with {stats_cmd}" msgstr "" -#: src/modules/ranks/ui/overview.py:407 +#: src/modules/ranks/ui/overview.py:426 msgctxt "ui:rank_overview|embed|field:note|value|voice_addendum" msgid "" "Also note that ranks will only be updated when a member leaves a tracked " @@ -744,32 +749,6 @@ msgid "" "members manually." msgstr "" -#: src/modules/ranks/ui/overview.py:415 -msgctxt "ui:rank_overview|embed:noranks|desc" -msgid "" -"No activity ranks have been set up!\n" -"Press 'AUTO' to automatically create a standard heirachy of voice | text | " -"xp ranks, or select a role or press Create below!" -msgstr "" - -#: src/modules/ranks/ui/overview.py:423 -#, possible-python-brace-format -msgctxt "ui:rank_overview|embed|title|type:voice" -msgid "Voice Ranks in {guild_name}" -msgstr "" - -#: src/modules/ranks/ui/overview.py:428 -#, possible-python-brace-format -msgctxt "ui:rank_overview|embed|title|type:xp" -msgid "XP ranks in {guild_name}" -msgstr "" - -#: src/modules/ranks/ui/overview.py:433 -#, possible-python-brace-format -msgctxt "ui:rank_overview|embed|title|type:message" -msgid "Message ranks in {guild_name}" -msgstr "" - #: src/modules/ranks/ui/editor.py:33 msgctxt "ui:rank_editor|input:role_name|label" msgid "Role Name" diff --git a/locales/templates/reminders.pot b/locales/templates/reminders.pot index efc7a880..5d894eef 100644 --- a/locales/templates/reminders.pot +++ b/locales/templates/reminders.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/locales/templates/rolemenus.pot b/locales/templates/rolemenus.pot index ee890f26..a7a96790 100644 --- a/locales/templates/rolemenus.pot +++ b/locales/templates/rolemenus.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -349,13 +349,18 @@ msgctxt "cmd:rolemenu_edit|parse:template|success:custom" msgid "Now using a custom menu message." msgstr "" -#: src/modules/rolemenus/cog.py:994 +#: src/modules/rolemenus/cog.py:986 +msgctxt "cmd:rolemenu_edit|parse:custom_message|success" +msgid "Custom menu message updated." +msgstr "" + +#: src/modules/rolemenus/cog.py:1001 #, possible-python-brace-format msgctxt "cmd:rolemenu_edit|repost|success" msgid "The role menu is now available at {message}" msgstr "" -#: src/modules/rolemenus/cog.py:1005 +#: src/modules/rolemenus/cog.py:1012 #, possible-python-brace-format msgctxt "cmd:rolemenu_edit|repost|error:forbidden" msgid "" @@ -363,7 +368,7 @@ msgid "" "permission in {channel}." msgstr "" -#: src/modules/rolemenus/cog.py:1010 +#: src/modules/rolemenus/cog.py:1017 #, possible-python-brace-format msgctxt "cmd:rolemenu_edit|repost|error:unknown" msgid "" @@ -371,40 +376,40 @@ msgid "" "**Error:** `{exception}`" msgstr "" -#: src/modules/rolemenus/cog.py:1044 +#: src/modules/rolemenus/cog.py:1051 msgctxt "cmd:rolemenu_delete" msgid "delmenu" msgstr "" -#: src/modules/rolemenus/cog.py:1047 +#: src/modules/rolemenus/cog.py:1054 msgctxt "cmd:rolemenu_delete|desc" msgid "Delete a role menu." msgstr "" -#: src/modules/rolemenus/cog.py:1051 +#: src/modules/rolemenus/cog.py:1058 msgctxt "cmd:rolemenu_delete|param:name" msgid "menu" msgstr "" -#: src/modules/rolemenus/cog.py:1056 +#: src/modules/rolemenus/cog.py:1063 msgctxt "cmd:rolemenu_delete|param:name|desc" msgid "Name of the rolemenu to delete." msgstr "" -#: src/modules/rolemenus/cog.py:1071 +#: src/modules/rolemenus/cog.py:1078 msgctxt "cmd:rolemenu_delete|error:author_perms" msgid "" "You need the `MANAGE_ROLES` permission in order to manage the server role " "menus." msgstr "" -#: src/modules/rolemenus/cog.py:1094 +#: src/modules/rolemenus/cog.py:1101 #, possible-python-brace-format msgctxt "cmd:rolemenu_delete|error:menu_not_found" msgid "This server does not have a role menu called `{name}`!" msgstr "" -#: src/modules/rolemenus/cog.py:1102 +#: src/modules/rolemenus/cog.py:1109 #, possible-python-brace-format msgctxt "cmd:rolemenu_delete|confirm|title" msgid "" @@ -412,272 +417,272 @@ msgid "" "reversible!" msgstr "" -#: src/modules/rolemenus/cog.py:1107 +#: src/modules/rolemenus/cog.py:1114 msgctxt "cmd:rolemenu_delete|confirm|button:yes" msgid "Yes, Delete Now" msgstr "" -#: src/modules/rolemenus/cog.py:1112 +#: src/modules/rolemenus/cog.py:1119 msgctxt "cmd:rolemenu_delete|confirm|button:no" msgid "No, Cancel" msgstr "" -#: src/modules/rolemenus/cog.py:1137 +#: src/modules/rolemenus/cog.py:1144 #, possible-python-brace-format msgctxt "cmd:rolemenu_delete|success|desc" msgid "Successfully deleted the menu **{name}**" msgstr "" -#: src/modules/rolemenus/cog.py:1145 +#: src/modules/rolemenus/cog.py:1152 msgctxt "cmd:rolemenu_addrole" msgid "addrole" msgstr "" -#: src/modules/rolemenus/cog.py:1148 +#: src/modules/rolemenus/cog.py:1155 msgctxt "cmd:rolemenu_addrole|desc" msgid "Add a new role to an existing role menu." msgstr "" -#: src/modules/rolemenus/cog.py:1153 +#: src/modules/rolemenus/cog.py:1160 msgctxt "cmd:rolemenu_addrole|param:menu" msgid "menu" msgstr "" -#: src/modules/rolemenus/cog.py:1156 +#: src/modules/rolemenus/cog.py:1163 msgctxt "cmd:rolemenu_addrole|param:role" msgid "role" msgstr "" -#: src/modules/rolemenus/cog.py:1167 +#: src/modules/rolemenus/cog.py:1174 msgctxt "cmd:rolemenu_addrole|param:menu|desc" msgid "Name of the menu to add a role to" msgstr "" -#: src/modules/rolemenus/cog.py:1171 +#: src/modules/rolemenus/cog.py:1178 msgctxt "cmd:rolemenu_addrole|param:role|desc" msgid "Role to add to the menu" msgstr "" -#: src/modules/rolemenus/cog.py:1179 +#: src/modules/rolemenus/cog.py:1186 msgctxt "cmd:rolemenu_addrole|param:duration|desc" msgid "Lifetime of the role after selection in minutes." msgstr "" -#: src/modules/rolemenus/cog.py:1227 +#: src/modules/rolemenus/cog.py:1234 #, possible-python-brace-format msgctxt "cmd:rolemenu_addrole|error:menu_not_found" msgid "This server does not have a role menu called `{name}`!" msgstr "" -#: src/modules/rolemenus/cog.py:1312 +#: src/modules/rolemenus/cog.py:1319 msgctxt "cmd:rolemenu_addrole|success:create|title" msgid "Added Menu Role" msgstr "" -#: src/modules/rolemenus/cog.py:1316 +#: src/modules/rolemenus/cog.py:1323 #, possible-python-brace-format msgctxt "cmd:rolemenu_addrole|success:create|desc" msgid "Add the role {role} to the menu **{menu}**." msgstr "" -#: src/modules/rolemenus/cog.py:1334 +#: src/modules/rolemenus/cog.py:1341 msgctxt "cmd:rolemenu_addrole|success:edit|title" msgid "Menu Role updated" msgstr "" -#: src/modules/rolemenus/cog.py:1346 +#: src/modules/rolemenus/cog.py:1353 #, possible-python-brace-format msgctxt "cmd:rolemenu_addrole|error:role_exists" msgid "The role {role} is already selectable from the menu **{menu}**" msgstr "" -#: src/modules/rolemenus/cog.py:1364 +#: src/modules/rolemenus/cog.py:1371 msgctxt "cmd:rolemenu_addrole|success|error:reaction|name" msgid "Note" msgstr "" -#: src/modules/rolemenus/cog.py:1376 +#: src/modules/rolemenus/cog.py:1383 msgctxt "cmd:rolemenu_addrole|success|button:editor|label" msgid "Edit Menu" msgstr "" -#: src/modules/rolemenus/cog.py:1393 +#: src/modules/rolemenus/cog.py:1400 msgctxt "cmd:rolemenu_editrole" msgid "editrole" msgstr "" -#: src/modules/rolemenus/cog.py:1396 +#: src/modules/rolemenus/cog.py:1403 msgctxt "cmd:rolemenu_editrole|desc" msgid "Edit role options in an existing role menu." msgstr "" -#: src/modules/rolemenus/cog.py:1401 +#: src/modules/rolemenus/cog.py:1408 msgctxt "cmd:rolemenu_editrole|param:menu" msgid "menu" msgstr "" -#: src/modules/rolemenus/cog.py:1404 +#: src/modules/rolemenus/cog.py:1411 msgctxt "cmd:rolemenu_editrole|param:menu_role" msgid "menu_role" msgstr "" -#: src/modules/rolemenus/cog.py:1407 +#: src/modules/rolemenus/cog.py:1414 msgctxt "cmd:rolemenu_editrole|param:role" msgid "new_role" msgstr "" -#: src/modules/rolemenus/cog.py:1418 +#: src/modules/rolemenus/cog.py:1425 msgctxt "cmd:rolemenu_editrole|param:menu|desc" msgid "Name of the menu to edit the role for" msgstr "" -#: src/modules/rolemenus/cog.py:1422 +#: src/modules/rolemenus/cog.py:1429 msgctxt "cmd:rolemenu_editrole|param:menu_role|desc" msgid "Label, name, or mention of the menu role to edit." msgstr "" -#: src/modules/rolemenus/cog.py:1426 +#: src/modules/rolemenus/cog.py:1433 msgctxt "cmd:rolemenu_editrole|param:role|desc" msgid "New server role this menu role should give." msgstr "" -#: src/modules/rolemenus/cog.py:1434 +#: src/modules/rolemenus/cog.py:1441 msgctxt "cmd:rolemenu_editrole|param:duration|desc" msgid "Lifetime of the role after selection in minutes." msgstr "" -#: src/modules/rolemenus/cog.py:1475 +#: src/modules/rolemenus/cog.py:1482 #, possible-python-brace-format msgctxt "cmd:rolemenu_editrole|error:menu_not_found" msgid "This server does not have a role menu called `{name}`!" msgstr "" -#: src/modules/rolemenus/cog.py:1503 +#: src/modules/rolemenus/cog.py:1510 #, possible-python-brace-format msgctxt "cmd:rolemenu_editrole|error:role_not_found" msgid "The menu **{menu}** does not have the role **{name}**" msgstr "" -#: src/modules/rolemenus/cog.py:1569 +#: src/modules/rolemenus/cog.py:1576 msgctxt "cmd:rolemenu_editrole|success|title" msgid "Role menu role updated" msgstr "" -#: src/modules/rolemenus/cog.py:1584 +#: src/modules/rolemenus/cog.py:1591 msgctxt "cmd:rolemenu_editrole|success|error:reaction|name" msgid "Warning!" msgstr "" -#: src/modules/rolemenus/cog.py:1609 +#: src/modules/rolemenus/cog.py:1616 msgctxt "cmd:rolemenu_delrole" msgid "delrole" msgstr "" -#: src/modules/rolemenus/cog.py:1612 +#: src/modules/rolemenus/cog.py:1619 msgctxt "cmd:rolemenu_delrole|desc" msgid "Remove a role from a role menu." msgstr "" -#: src/modules/rolemenus/cog.py:1616 +#: src/modules/rolemenus/cog.py:1623 msgctxt "cmd:rolemenu_delrole|param:menu" msgid "menu" msgstr "" -#: src/modules/rolemenus/cog.py:1617 +#: src/modules/rolemenus/cog.py:1624 msgctxt "cmd:rolemenu_delrole|param:menu_role" msgid "menu_role" msgstr "" -#: src/modules/rolemenus/cog.py:1622 +#: src/modules/rolemenus/cog.py:1629 msgctxt "cmd:rolemenu_delrole|param:menu|desc" msgid "Name of the menu to delete the role from." msgstr "" -#: src/modules/rolemenus/cog.py:1626 +#: src/modules/rolemenus/cog.py:1633 msgctxt "cmd:rolemenu_delrole|param:menu_role|desc" msgid "Name, label, or mention of the role to delete." msgstr "" -#: src/modules/rolemenus/cog.py:1644 +#: src/modules/rolemenus/cog.py:1651 msgctxt "cmd:rolemenu_delrole|error:author_perms" msgid "" "You need the `MANAGE_ROLES` permission in order to manage the server role " "menus." msgstr "" -#: src/modules/rolemenus/cog.py:1668 +#: src/modules/rolemenus/cog.py:1675 #, possible-python-brace-format msgctxt "cmd:rolemenu_delrole|error:menu_not_found" msgid "This server does not have a role menu called `{name}`!" msgstr "" -#: src/modules/rolemenus/cog.py:1696 +#: src/modules/rolemenus/cog.py:1703 #, possible-python-brace-format msgctxt "cmd:rolemenu_delrole|error:role_not_found" msgid "The menu **{menu}** does not have the role **{name}**" msgstr "" -#: src/modules/rolemenus/cog.py:1713 +#: src/modules/rolemenus/cog.py:1720 #, possible-python-brace-format msgctxt "cmd:rolemenu_delrole|success" msgid "The role **{name}** was successfully removed from the menu **{menu}**." msgstr "" -#: src/modules/rolemenus/roleoptions.py:54 +#: src/modules/rolemenus/roleoptions.py:57 msgctxt "roleset:role" msgid "role" msgstr "" -#: src/modules/rolemenus/roleoptions.py:57 +#: src/modules/rolemenus/roleoptions.py:60 msgctxt "roleset:role|desc" msgid "The role associated to this menu item." msgstr "" -#: src/modules/rolemenus/roleoptions.py:61 +#: src/modules/rolemenus/roleoptions.py:64 msgctxt "roleset:role|long_desc" msgid "The role given when this menu item is selected in the role menu." msgstr "" -#: src/modules/rolemenus/roleoptions.py:74 +#: src/modules/rolemenus/roleoptions.py:77 #, possible-python-brace-format msgctxt "roleset:role|set_response:set" msgid "This menu item will now give the role {role}." msgstr "" -#: src/modules/rolemenus/roleoptions.py:82 +#: src/modules/rolemenus/roleoptions.py:88 msgctxt "roleset:label" msgid "label" msgstr "" -#: src/modules/rolemenus/roleoptions.py:85 +#: src/modules/rolemenus/roleoptions.py:91 msgctxt "roleset:label|desc" msgid "A short button label for this role." msgstr "" -#: src/modules/rolemenus/roleoptions.py:90 +#: src/modules/rolemenus/roleoptions.py:96 msgctxt "roleset:label|long_desc" msgid "" "A short name for this role, to be displayed in button labels, dropdown " "titles, and some menu layouts. By default uses the Discord role name." msgstr "" -#: src/modules/rolemenus/roleoptions.py:104 +#: src/modules/rolemenus/roleoptions.py:110 #, possible-python-brace-format msgctxt "roleset:role|set_response" msgid "This menu role is now called `{value}`." msgstr "" -#: src/modules/rolemenus/roleoptions.py:112 +#: src/modules/rolemenus/roleoptions.py:118 msgctxt "roleset:emoji" msgid "emoji" msgstr "" -#: src/modules/rolemenus/roleoptions.py:115 +#: src/modules/rolemenus/roleoptions.py:121 msgctxt "roleset:emoji|desc" msgid "The emoji associated with this role." msgstr "" -#: src/modules/rolemenus/roleoptions.py:119 +#: src/modules/rolemenus/roleoptions.py:125 msgctxt "roleset:emoji|long_desc" msgid "" "The role emoji is used for the reaction (in reaction role menus), and " @@ -685,110 +690,120 @@ msgid "" "The emoji is also displayed next to the role in most menu templates." msgstr "" -#: src/modules/rolemenus/roleoptions.py:157 +#: src/modules/rolemenus/roleoptions.py:165 #, possible-python-brace-format msgctxt "roleset:emoji|error:test_emoji" msgid "The selected emoji `{emoji}` is invalid or has been deleted." msgstr "" -#: src/modules/rolemenus/roleoptions.py:168 +#: src/modules/rolemenus/roleoptions.py:176 #, possible-python-brace-format msgctxt "roleset:emoji|set_response:set" msgid "The menu role emoji is now {emoji}." msgstr "" -#: src/modules/rolemenus/roleoptions.py:173 +#: src/modules/rolemenus/roleoptions.py:181 msgctxt "roleset:emoji|set_response:unset" msgid "The menu role emoji has been removed." msgstr "" -#: src/modules/rolemenus/roleoptions.py:181 +#: src/modules/rolemenus/roleoptions.py:189 msgctxt "roleset:description" msgid "description" msgstr "" -#: src/modules/rolemenus/roleoptions.py:184 +#: src/modules/rolemenus/roleoptions.py:192 msgctxt "roleset:description|desc" msgid "A longer description of this role." msgstr "" -#: src/modules/rolemenus/roleoptions.py:189 +#: src/modules/rolemenus/roleoptions.py:197 msgctxt "roleset:description|long_desc" msgid "" "The description is displayed under the role label in dropdown style menus. " "It may also be used as a substitution key in custom role selection responses." msgstr "" -#: src/modules/rolemenus/roleoptions.py:205 +#: src/modules/rolemenus/roleoptions.py:213 msgctxt "roleset:description|set_response:set" msgid "The role description has been set." msgstr "" -#: src/modules/rolemenus/roleoptions.py:210 +#: src/modules/rolemenus/roleoptions.py:218 msgctxt "roleset:description|set_response:unset" msgid "The role description has been removed." msgstr "" -#: src/modules/rolemenus/roleoptions.py:218 +#: src/modules/rolemenus/roleoptions.py:226 msgctxt "roleset:price" msgid "price" msgstr "" -#: src/modules/rolemenus/roleoptions.py:221 -msgctxt "roleset:price|desc" -msgid "Price of the role, in LionCoins." -msgstr "" - -#: src/modules/rolemenus/roleoptions.py:225 -msgctxt "roleset:price|long_desc" -msgid "How much the role costs when selected, in LionCoins." -msgstr "" - #: src/modules/rolemenus/roleoptions.py:229 +msgctxt "roleset:price|desc" +msgid "Price of the role, in LionCoins. May be negative." +msgstr "" + +#: src/modules/rolemenus/roleoptions.py:233 +msgctxt "roleset:price|long_desc" +msgid "" +"How many LionCoins should be deducted from a member's account when they " +"equip this role through this menu.\n" +"The price may be negative, in which case the member will instead be rewarded " +"coins when they equip the role." +msgstr "" + +#: src/modules/rolemenus/roleoptions.py:240 msgctxt "roleset:price|accepts" -msgid "Amount of coins that the role costs." +msgid "Amount of coins that the role costs when equipped." msgstr "" -#: src/modules/rolemenus/roleoptions.py:242 +#: src/modules/rolemenus/roleoptions.py:254 #, possible-python-brace-format -msgctxt "roleset:price|set_response:set" -msgid "This role will now cost {price} to equip." +msgctxt "roleset:price|set_response:positive" +msgid "Equipping this role will now cost {coin}**{price}**." msgstr "" -#: src/modules/rolemenus/roleoptions.py:247 -msgctxt "roleset:price|set_response:unset" -msgid "This role will now be free to equip from this role menu." +#: src/modules/rolemenus/roleoptions.py:259 +msgctxt "roleset:price|set_response:zero" +msgid "Equipping this role is now free." msgstr "" -#: src/modules/rolemenus/roleoptions.py:255 +#: src/modules/rolemenus/roleoptions.py:264 +#, possible-python-brace-format +msgctxt "roleset:price|set_response:negative" +msgid "Equipping this role will now reward {coin}**{price}**." +msgstr "" + +#: src/modules/rolemenus/roleoptions.py:272 msgctxt "roleset:duration" msgid "duration" msgstr "" -#: src/modules/rolemenus/roleoptions.py:258 +#: src/modules/rolemenus/roleoptions.py:275 msgctxt "roleset:duration|desc" msgid "Lifetime of the role after selection" msgstr "" -#: src/modules/rolemenus/roleoptions.py:262 +#: src/modules/rolemenus/roleoptions.py:279 msgctxt "roleset:duration|long_desc" msgid "" "Allows creation of 'temporary roles' which expire a given time after being " "equipped. Refunds will not be given upon expiry." msgstr "" -#: src/modules/rolemenus/roleoptions.py:267 +#: src/modules/rolemenus/roleoptions.py:284 msgctxt "roleset:duration|notset" msgid "Forever." msgstr "" -#: src/modules/rolemenus/roleoptions.py:280 +#: src/modules/rolemenus/roleoptions.py:297 #, possible-python-brace-format msgctxt "roleset:duration|set_response:set" msgid "This role will now expire after {duration}." msgstr "" -#: src/modules/rolemenus/roleoptions.py:285 +#: src/modules/rolemenus/roleoptions.py:302 msgctxt "roleset:duration|set_response:unset" msgid "This role will no longer expire after being selected." msgstr "" @@ -893,25 +908,31 @@ msgctxt "rolemenu|deselect|success:refund|desc" msgid "You have removed **{role}**, and been refunded {coin} **{amount}**." msgstr "" -#: src/modules/rolemenus/rolemenu.py:540 +#: src/modules/rolemenus/rolemenu.py:541 +#, possible-python-brace-format +msgctxt "rolemenu|deselect|success:negrefund|desc" +msgid "You have removed **{role}**, and have lost {coin} **{amount}**." +msgstr "" + +#: src/modules/rolemenus/rolemenu.py:546 #, possible-python-brace-format msgctxt "rolemenu|deselect|success:norefund|desc" msgid "You have unequipped **{role}**." msgstr "" -#: src/modules/rolemenus/rolemenu.py:554 +#: src/modules/rolemenus/rolemenu.py:560 #, possible-python-brace-format msgctxt "rolemenu|select|error:required_role" -msgid "You need to have the **{role}** role to use this!" +msgid "You need to have the role **{role}** required to use this menu!" msgstr "" -#: src/modules/rolemenus/rolemenu.py:568 +#: src/modules/rolemenus/rolemenu.py:574 #, possible-python-brace-format msgctxt "rolemenu|select|error:max_obtainable" msgid "You already have the maximum of {obtainable} roles from this menu!" msgstr "" -#: src/modules/rolemenus/rolemenu.py:582 +#: src/modules/rolemenus/rolemenu.py:588 #, possible-python-brace-format msgctxt "rolemenu|select|error:insufficient_funds" msgid "" @@ -919,41 +940,41 @@ msgid "" "**{balance}**!" msgstr "" -#: src/modules/rolemenus/rolemenu.py:598 +#: src/modules/rolemenus/rolemenu.py:604 msgctxt "rolemenu|select|error:perms" msgid "I don't have enough permissions to give you this role!" msgstr "" -#: src/modules/rolemenus/rolemenu.py:605 +#: src/modules/rolemenus/rolemenu.py:611 msgctxt "rolemenu|select|error:discord" msgid "" "An unknown error occurred while assigning your role! Please try again later." msgstr "" -#: src/modules/rolemenus/rolemenu.py:647 +#: src/modules/rolemenus/rolemenu.py:653 msgctxt "rolemenu|select|success|title" msgid "Role equipped" msgstr "" -#: src/modules/rolemenus/rolemenu.py:653 +#: src/modules/rolemenus/rolemenu.py:659 #, possible-python-brace-format msgctxt "rolemenu|select|success:purchase|desc" msgid "You have purchased the role **{role}** for {coin}**{amount}**" msgstr "" -#: src/modules/rolemenus/rolemenu.py:658 +#: src/modules/rolemenus/rolemenu.py:664 #, possible-python-brace-format msgctxt "rolemenu|select|success:nopurchase|desc" msgid "You have equipped the role **{role}**" msgstr "" -#: src/modules/rolemenus/rolemenu.py:664 +#: src/modules/rolemenus/rolemenu.py:670 #, possible-python-brace-format msgctxt "rolemenu|select|expires_at" msgid "The role will expire at {timestamp}." msgstr "" -#: src/modules/rolemenus/rolemenu.py:717 +#: src/modules/rolemenus/rolemenu.py:724 #, possible-python-brace-format msgctxt "rolemenu|content:reactions" msgid "[Click here]({jump_link}) to jump back." @@ -1200,251 +1221,251 @@ msgctxt "ui:menu_editor|button:bulk_edit|modal|title" msgid "Menu Options" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:215 +#: src/modules/rolemenus/ui/menueditor.py:218 msgctxt "ui:menu_editor|button:bulk_edit|label" msgid "Bulk Edit" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:235 +#: src/modules/rolemenus/ui/menueditor.py:238 msgctxt "ui:menu_editor|button:sticky|label" msgid "Toggle Sticky" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:259 +#: src/modules/rolemenus/ui/menueditor.py:262 msgctxt "ui:menu_editor|button:refunds|label" msgid "Toggle Refunds" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:289 +#: src/modules/rolemenus/ui/menueditor.py:292 msgctxt "ui:menu_editor|menu:reqroles|placeholder" msgid "Select Required Role" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:308 +#: src/modules/rolemenus/ui/menueditor.py:311 msgctxt "ui:menu_editor|button:modify_roles|label" msgid "Modify Roles" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:337 +#: src/modules/rolemenus/ui/menueditor.py:340 msgctxt "ui:menu_editor|role_editor|modal|title" msgid "Edit Menu Role" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:411 +#: src/modules/rolemenus/ui/menueditor.py:416 msgctxt "ui:menu_editor|menu:add_roles|error:too_many_reactions" msgid "Too many roles! Reaction role menus cannot exceed `20` roles." msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:416 +#: src/modules/rolemenus/ui/menueditor.py:421 msgctxt "ui:menu_editor|menu:add_roles|error:too_many_roles" msgid "Too many roles! Role menus cannot have more than `25` roles." msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:455 +#: src/modules/rolemenus/ui/menueditor.py:460 msgctxt "ui:menu_editor|menu:add_roles|placeholder" msgid "Add Roles" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:481 +#: src/modules/rolemenus/ui/menueditor.py:486 msgctxt "ui:menu_editor|menu:edit_roles|placeholder" msgid "Edit Roles" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:514 +#: src/modules/rolemenus/ui/menueditor.py:524 msgctxt "ui:menu_editor|menu:del_role|placeholder" msgid "Remove Roles" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:538 +#: src/modules/rolemenus/ui/menueditor.py:548 msgctxt "ui:menu_editor|button:style|error:non-managed" msgid "" "Cannot change the style of a menu attached to a message I did not send! " "Please repost first." msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:551 +#: src/modules/rolemenus/ui/menueditor.py:561 msgctxt "ui:menu_editor|button:style|label" msgid "Menu Style" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:576 +#: src/modules/rolemenus/ui/menueditor.py:586 msgctxt "ui:menu_editor|menu:style|error:too_many_reactions" msgid "" "Too many roles! The Reaction style is limited to `20` roles (Discord " "limitation)." msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:586 +#: src/modules/rolemenus/ui/menueditor.py:596 msgctxt "ui:menu_editor|menu:style|error:incomplete_emojis" msgid "" "Cannot switch to the Reaction Role Style! Every role needs to have a " "distinct emoji first." msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:602 +#: src/modules/rolemenus/ui/menueditor.py:614 msgctxt "ui:menu_editor|menu:style|placeholder" msgid "Select Menu Style" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:606 +#: src/modules/rolemenus/ui/menueditor.py:618 msgctxt "ui:menu_editor|menu:style|option:reaction|label" msgid "Reaction Roles" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:609 +#: src/modules/rolemenus/ui/menueditor.py:621 msgctxt "ui:menu_editor|menu:style|option:reaction|desc" msgid "Roles are represented compactly as clickable reactions on a message." msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:615 +#: src/modules/rolemenus/ui/menueditor.py:627 msgctxt "ui:menu_editor|menu:style|option:button|label" msgid "Button Menu" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:618 +#: src/modules/rolemenus/ui/menueditor.py:630 msgctxt "ui:menu_editor|menu:style|option:button|desc" msgid "" "Roles are represented in 5 rows of 5 buttons, each with an emoji and label." msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:624 +#: src/modules/rolemenus/ui/menueditor.py:636 msgctxt "ui:menu_editor|menu:style|option:dropdown|label" msgid "Dropdown Menu" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:627 +#: src/modules/rolemenus/ui/menueditor.py:639 msgctxt "ui:menu_editor|menu:style|option:dropdown|desc" msgid "Roles are selectable from a dropdown menu below the message." msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:697 +#: src/modules/rolemenus/ui/menueditor.py:709 msgctxt "ui:menu_editor|menu:template|placeholder" msgid "Select Message Template" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:707 +#: src/modules/rolemenus/ui/menueditor.py:719 msgctxt "ui:menu_editor|menu:template|option:custom|label" msgid "Custom Message" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:712 +#: src/modules/rolemenus/ui/menueditor.py:724 msgctxt "ui:menu_editor|menu:template|option:custom|description" msgid "Entirely custom menu message (opens an interactive editor)." msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:729 +#: src/modules/rolemenus/ui/menueditor.py:741 msgctxt "ui:menu_editor|button:delete|confirm|title" msgid "Are you sure you want to delete this menu? This is not reversible!" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:734 +#: src/modules/rolemenus/ui/menueditor.py:746 msgctxt "ui:menu_editor|button:delete|confirm|button:yes" msgid "Yes, Delete Now" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:739 +#: src/modules/rolemenus/ui/menueditor.py:751 msgctxt "ui:menu_editor|button:delete|confirm|button:no" msgid "No, Go Back" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:756 +#: src/modules/rolemenus/ui/menueditor.py:768 msgctxt "ui:menu_editor|button:delete|label" msgid "Delete Menu" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:813 +#: src/modules/rolemenus/ui/menueditor.py:825 msgctxt "ui:menu_editor|button:edit_msg|label" msgid "Edit Message" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:844 +#: src/modules/rolemenus/ui/menueditor.py:851 msgctxt "ui:menu_editor|button:preview|label" msgid "Preview" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:860 +#: src/modules/rolemenus/ui/menueditor.py:867 msgctxt "ui:menu_editor|button:repost|widget:repost|menu:channel|placeholder" msgid "Select New Channel" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:878 +#: src/modules/rolemenus/ui/menueditor.py:885 msgctxt "ui:menu_editor|button:repost|widget:repost|error:perms|title" msgid "Insufficient Permissions!" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:882 +#: src/modules/rolemenus/ui/menueditor.py:889 msgctxt "ui:menu_editor|button:repost|eidget:repost|error:perms|desc" msgid "I lack the `EMBED_LINKS` or `SEND_MESSAGES` permission in this channel." msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:895 +#: src/modules/rolemenus/ui/menueditor.py:902 #, possible-python-brace-format msgctxt "ui:menu_editor|button:repost|widget:repost|error:post_failed" msgid "" -"An error ocurred while posting to {channel}. Do I have sufficient " -"permissions?" +"An unknown error ocurred while posting to {channel}!\n" +"**Error:** `{exception}`" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:907 +#: src/modules/rolemenus/ui/menueditor.py:915 msgctxt "ui:menu_editor|button:repost|widget:repost|success|title" msgid "Role Menu Moved" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:913 +#: src/modules/rolemenus/ui/menueditor.py:921 #, possible-python-brace-format msgctxt "ui:menu_editor|button:repost|widget:repost|success|desc:general" msgid "The role menu `{name}` is now available at {message_link}." msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:927 +#: src/modules/rolemenus/ui/menueditor.py:935 msgctxt "ui:menu_editor|button:repost|widget:repost|success|desc:reactions" msgid "Please check the message reactions are correct." msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:940 +#: src/modules/rolemenus/ui/menueditor.py:948 msgctxt "ui:menu_editor|button:repost|widget:repost|title" msgid "Repost Role Menu" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:944 +#: src/modules/rolemenus/ui/menueditor.py:952 msgctxt "ui:menu_editor|button:repost|widget:repost|description" msgid "Please select the channel to which you want to resend this menu." msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:959 +#: src/modules/rolemenus/ui/menueditor.py:967 msgctxt "ui:menu_editor|button:repost|label:repost" msgid "Repost" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:964 +#: src/modules/rolemenus/ui/menueditor.py:972 msgctxt "ui:menu_editor|button:repost|label:post" msgid "Post" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:974 +#: src/modules/rolemenus/ui/menueditor.py:982 msgctxt "ui:menu_editor|embed|title" msgid "Role Menu Editor" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:983 +#: src/modules/rolemenus/ui/menueditor.py:991 #, possible-python-brace-format msgctxt "ui:menu_editor|embed|description|jump_text:attached" msgid "Members may use this menu from {jump_url}" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:988 +#: src/modules/rolemenus/ui/menueditor.py:996 msgctxt "ui:menu_editor|embed|description|jump_text:unattached" msgid "" "This menu is not currently active!\n" "Make it available by clicking `Post` below." msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:1002 +#: src/modules/rolemenus/ui/menueditor.py:1010 msgctxt "ui:menu_editor|embed|field:tips|name" msgid "Command Tips" msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:1006 +#: src/modules/rolemenus/ui/menueditor.py:1014 #, possible-python-brace-format msgctxt "ui:menu_editor|embed|field:tips|value" msgid "" @@ -1454,12 +1475,12 @@ msgid "" "{editrole} to edit role options." msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:1046 +#: src/modules/rolemenus/ui/menueditor.py:1054 msgctxt "ui:menu_editor|error:invald_emoji|title" msgid "Invalid emoji encountered." msgstr "" -#: src/modules/rolemenus/ui/menueditor.py:1050 +#: src/modules/rolemenus/ui/menueditor.py:1058 #, possible-python-brace-format msgctxt "ui:menu_editor|error:invalid_emoji|desc" msgid "" diff --git a/locales/templates/rooms.pot b/locales/templates/rooms.pot index 72574608..ce288631 100644 --- a/locales/templates/rooms.pot +++ b/locales/templates/rooms.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/locales/templates/schedule.pot b/locales/templates/schedule.pot index a6426401..6b191179 100644 --- a/locales/templates/schedule.pot +++ b/locales/templates/schedule.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,25 +18,25 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" -#: src/modules/schedule/cog.py:429 +#: src/modules/schedule/cog.py:478 msgctxt "create_booking|error:no_lobby" msgid "" "This server has not set a `session_lobby`, so the scheduled session system " "is disabled!" msgstr "" -#: src/modules/schedule/cog.py:441 +#: src/modules/schedule/cog.py:490 msgctxt "create_booking|error:no_member" msgid "An unknown Discord error occurred. Please try again in a few minutes." msgstr "" -#: src/modules/schedule/cog.py:449 +#: src/modules/schedule/cog.py:498 msgctxt "create_booking|error:blacklisted" msgid "" "You have been blacklisted from the scheduled session system in this server." msgstr "" -#: src/modules/schedule/cog.py:460 +#: src/modules/schedule/cog.py:509 #, possible-python-brace-format msgctxt "create_booking|error:insufficient_balance" msgid "" @@ -48,22 +48,22 @@ msgid_plural "" msgstr[0] "" msgstr[1] "" -#: src/modules/schedule/cog.py:474 +#: src/modules/schedule/cog.py:523 msgctxt "create_booking|error:already_booked" msgid "One or more requested timeslots are already booked!" msgstr "" -#: src/modules/schedule/cog.py:677 +#: src/modules/schedule/cog.py:726 msgctxt "cmd:schedule" msgid "schedule" msgstr "" -#: src/modules/schedule/cog.py:680 +#: src/modules/schedule/cog.py:729 msgctxt "cmd:schedule|desc" msgid "View and manage your scheduled session." msgstr "" -#: src/modules/schedule/cog.py:708 +#: src/modules/schedule/cog.py:757 #, possible-python-brace-format msgctxt "cmd:schedule|cancel_booking|error:parse_slot" msgid "" @@ -71,25 +71,25 @@ msgid "" "from the autocomplete options." msgstr "" -#: src/modules/schedule/cog.py:716 +#: src/modules/schedule/cog.py:765 #, possible-python-brace-format msgctxt "cmd:schedule|cancel_booking|error:not_booked" msgid "Could not cancel {time} booking because it is not booked!" msgstr "" -#: src/modules/schedule/cog.py:725 +#: src/modules/schedule/cog.py:774 #, possible-python-brace-format msgctxt "cmd:schedule|cancel_booking|error:too_soon" msgid "Cannot cancel {time} booking because it is running or starting soon!" msgstr "" -#: src/modules/schedule/cog.py:738 +#: src/modules/schedule/cog.py:787 #, possible-python-brace-format msgctxt "cmd:schedule|cancel_booking|success" msgid "Successfully cancelled your booking at {time}." msgstr "" -#: src/modules/schedule/cog.py:751 +#: src/modules/schedule/cog.py:800 #, possible-python-brace-format msgctxt "cmd:schedule|create_booking|error:parse_slot" msgid "" @@ -97,30 +97,30 @@ msgid "" "from the autocomplete options." msgstr "" -#: src/modules/schedule/cog.py:759 +#: src/modules/schedule/cog.py:808 #, possible-python-brace-format msgctxt "cmd:schedule|create_booking|error:already_booked" msgid "You have already booked a scheduled session for {time}." msgstr "" -#: src/modules/schedule/cog.py:768 +#: src/modules/schedule/cog.py:817 #, possible-python-brace-format msgctxt "cmd:schedule|create_booking|error:too_soon" msgid "Cannot book session at {time} because it is running or starting soon!" msgstr "" -#: src/modules/schedule/cog.py:780 +#: src/modules/schedule/cog.py:829 #, possible-python-brace-format msgctxt "cmd:schedule|create_booking|success" msgid "You have successfully scheduled a session at {time}." msgstr "" -#: src/modules/schedule/cog.py:847 +#: src/modules/schedule/cog.py:896 msgctxt "cmd:configure_schedule" msgid "schedule" msgstr "" -#: src/modules/schedule/cog.py:850 +#: src/modules/schedule/cog.py:899 msgctxt "cmd:configure_schedule|desc" msgid "Configure Scheduled Session system" msgstr "" @@ -841,7 +841,7 @@ msgid "" "`MANAGE_WEBHOOKS` permission." msgstr "" -#: src/modules/schedule/core/session.py:274 +#: src/modules/schedule/core/session.py:280 #, possible-python-brace-format msgctxt "session|prepare|error:room_permissions" msgid "" @@ -850,7 +850,7 @@ msgid "" "`VIEW_CHANNEL` permissions." msgstr "" -#: src/modules/schedule/core/session.py:317 +#: src/modules/schedule/core/session.py:330 #, possible-python-brace-format msgctxt "session|open|error:room_permissions" msgid "" @@ -859,57 +859,57 @@ msgid "" "`VIEW_CHANNEL` permissions." msgstr "" -#: src/modules/schedule/core/session.py:358 +#: src/modules/schedule/core/session.py:371 #, possible-python-brace-format msgctxt "session|status|title" msgid "Session {start} - {end}" msgstr "" -#: src/modules/schedule/core/session.py:369 +#: src/modules/schedule/core/session.py:382 msgctxt "session|status|desc:cancelled" msgid "" "I cancelled this scheduled session because I was unavailable. All members " "who booked the session have been refunded." msgstr "" -#: src/modules/schedule/core/session.py:376 +#: src/modules/schedule/core/session.py:389 msgctxt "session|status|desc:no_members" msgid "*No members scheduled this session.*" msgstr "" -#: src/modules/schedule/core/session.py:382 +#: src/modules/schedule/core/session.py:395 #, possible-python-brace-format msgctxt "session|status:preparing|desc:has_members" msgid "Starting {start}" msgstr "" -#: src/modules/schedule/core/session.py:385 +#: src/modules/schedule/core/session.py:398 msgctxt "session|status:preparing|field:members" msgid "Members" msgstr "" -#: src/modules/schedule/core/session.py:392 +#: src/modules/schedule/core/session.py:405 #, possible-python-brace-format msgctxt "session|status:running|desc:has_members" msgid "Finishing {start}" msgstr "" -#: src/modules/schedule/core/session.py:426 +#: src/modules/schedule/core/session.py:439 msgctxt "session|status:running|field:waiting" msgid "Waiting For" msgstr "" -#: src/modules/schedule/core/session.py:432 +#: src/modules/schedule/core/session.py:445 msgctxt "session|status:running|field:attending" msgid "Attending" msgstr "" -#: src/modules/schedule/core/session.py:438 +#: src/modules/schedule/core/session.py:451 msgctxt "session|status:running|field:attended" msgid "Attended" msgstr "" -#: src/modules/schedule/core/session.py:463 +#: src/modules/schedule/core/session.py:476 #, possible-python-brace-format msgctxt "session|status:finished|desc:everyone_att" msgid "" @@ -917,7 +917,7 @@ msgid "" "**{reward} + {bonus}**!" msgstr "" -#: src/modules/schedule/core/session.py:474 +#: src/modules/schedule/core/session.py:487 #, possible-python-brace-format msgctxt "session|status:finished|desc:some_att" msgid "" @@ -927,7 +927,7 @@ msgid "" "without refund!*" msgstr "" -#: src/modules/schedule/core/session.py:486 +#: src/modules/schedule/core/session.py:499 msgctxt "session|status:finished|desc:some_att" msgid "" "No-one attended this session! No-one received rewards.\n" @@ -935,12 +935,12 @@ msgid "" "without refund!*" msgstr "" -#: src/modules/schedule/core/session.py:492 +#: src/modules/schedule/core/session.py:505 msgctxt "session|status:finished|field:attended" msgid "Attended" msgstr "" -#: src/modules/schedule/core/session.py:497 +#: src/modules/schedule/core/session.py:510 msgctxt "session|status:finished|field:missing" msgid "Missing" msgstr "" diff --git a/locales/templates/settings_base.pot b/locales/templates/settings_base.pot index ff6959cc..dec52067 100644 --- a/locales/templates/settings_base.pot +++ b/locales/templates/settings_base.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -84,91 +84,91 @@ msgctxt "settype:role|accepts" msgid "A role name or id" msgstr "" -#: src/settings/setting_types.py:427 +#: src/settings/setting_types.py:430 #, possible-python-brace-format msgctxt "settype:role|parse|error:not_found" msgid "Role `{string}` could not be found in this guild!" msgstr "" -#: src/settings/setting_types.py:492 +#: src/settings/setting_types.py:495 msgctxt "settype:bool|accepts" msgid "Enabled/Disabled" msgstr "" -#: src/settings/setting_types.py:497 +#: src/settings/setting_types.py:500 msgctxt "settype:bool|parse:truthy_values" msgid "enabled|yes|true|on|enable|1" msgstr "" -#: src/settings/setting_types.py:501 +#: src/settings/setting_types.py:504 msgctxt "settype:bool|parse:falsey_values" msgid "disabled|no|false|off|disable|0" msgstr "" -#: src/settings/setting_types.py:506 +#: src/settings/setting_types.py:509 msgctxt "settype:bool|output:true" msgid "On" msgstr "" -#: src/settings/setting_types.py:507 +#: src/settings/setting_types.py:510 msgctxt "settype:bool|output:false" msgid "Off" msgstr "" -#: src/settings/setting_types.py:508 +#: src/settings/setting_types.py:511 msgctxt "settype:bool|output:none" msgid "Not Set" msgstr "" -#: src/settings/setting_types.py:619 +#: src/settings/setting_types.py:622 msgctxt "settype:integer|accepts" msgid "An integer" msgstr "" -#: src/settings/setting_types.py:682 +#: src/settings/setting_types.py:685 msgctxt "settype:emoji|desc" msgid "Unicode or custom emoji" msgstr "" -#: src/settings/setting_types.py:754 +#: src/settings/setting_types.py:757 msgctxt "settype:guildid|accepts" msgid "Any Snowflake ID" msgstr "" -#: src/settings/setting_types.py:823 +#: src/settings/setting_types.py:826 msgctxt "settype:timezone|accepts" msgid "A timezone name from the 'tz database' (e.g. 'Europe/London')" msgstr "" -#: src/settings/setting_types.py:893 +#: src/settings/setting_types.py:896 msgctxt "settype:timezone|summary_table|field:supported|key" msgid "Supported" msgstr "" -#: src/settings/setting_types.py:897 +#: src/settings/setting_types.py:900 #, possible-python-brace-format msgctxt "settype:timezone|summary_table|field:supported|value" msgid "Any timezone from the [tz database]({link})." msgstr "" -#: src/settings/setting_types.py:914 +#: src/settings/setting_types.py:917 #, possible-python-brace-format msgctxt "set_type:timezone|acmpl|no_matching" msgid "No timezones matching '{input}'!" msgstr "" -#: src/settings/setting_types.py:927 +#: src/settings/setting_types.py:930 #, possible-python-brace-format msgctxt "set_type:timezone|acmpl|choice" msgid "{tz} (Currently {now})" msgstr "" -#: src/settings/setting_types.py:957 +#: src/settings/setting_types.py:960 msgctxt "settype:timestamp|accepts" msgid "A timestamp in the form YYYY-MM-DD HH:MM" msgstr "" -#: src/settings/setting_types.py:986 +#: src/settings/setting_types.py:989 #, possible-python-brace-format msgctxt "settype:timestamp|parse|error:invalid" msgid "" @@ -176,43 +176,43 @@ msgid "" "format." msgstr "" -#: src/settings/setting_types.py:1017 +#: src/settings/setting_types.py:1020 msgctxt "settype:raw|accepts" msgid "Anything" msgstr "" -#: src/settings/setting_types.py:1070 +#: src/settings/setting_types.py:1073 msgctxt "settype:enum|accepts" msgid "A valid option." msgstr "" -#: src/settings/setting_types.py:1120 +#: src/settings/setting_types.py:1123 #, possible-python-brace-format msgctxt "settype:enum|parse|error:not_found" msgid "`{provided}` is not a valid option!" msgstr "" -#: src/settings/setting_types.py:1168 +#: src/settings/setting_types.py:1171 msgctxt "settype:duration|accepts" msgid "A number of days, hours, minutes, and seconds, e.g. `2d 4h 10s`." msgstr "" -#: src/settings/setting_types.py:1349 +#: src/settings/setting_types.py:1352 msgctxt "settype:channel_list|accepts" msgid "Comma separated list of channel ids." msgstr "" -#: src/settings/setting_types.py:1360 +#: src/settings/setting_types.py:1363 msgctxt "settype:role_list|accepts" msgid "Comma separated list of role ids." msgstr "" -#: src/settings/setting_types.py:1376 +#: src/settings/setting_types.py:1379 msgctxt "settype:stringlist|accepts" msgid "Comma separated strings." msgstr "" -#: src/settings/setting_types.py:1387 +#: src/settings/setting_types.py:1390 msgctxt "settype:guildidlist|accepts" msgid "Comma separated list of guild ids." msgstr "" diff --git a/locales/templates/shop.pot b/locales/templates/shop.pot index d8a6b050..d44f62eb 100644 --- a/locales/templates/shop.pot +++ b/locales/templates/shop.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -32,27 +32,27 @@ msgctxt "cmd:shop|desc" msgid "Purchase coloures, roles, and other goodies with LionCoins." msgstr "" -#: src/modules/shop/cog.py:124 +#: src/modules/shop/cog.py:125 msgctxt "cmd:shop_open" msgid "open" msgstr "" -#: src/modules/shop/cog.py:125 +#: src/modules/shop/cog.py:126 msgctxt "cmd:shop_open|desc" msgid "Open the server shop." msgstr "" -#: src/modules/shop/cog.py:151 +#: src/modules/shop/cog.py:153 msgctxt "cmd:shop_open|error:no_shops" msgid "There is nothing to buy!" msgstr "" -#: src/modules/shop/cog.py:213 +#: src/modules/shop/cog.py:215 msgctxt "ui:stores|button:close|label" msgid "Close" msgstr "" -#: src/modules/shop/cog.py:220 +#: src/modules/shop/cog.py:222 msgctxt "ui:stores|button:close|response|title" msgid "Shop Closed" msgstr "" diff --git a/locales/templates/statistics.pot b/locales/templates/statistics.pot index d034783d..e4d2c67e 100644 --- a/locales/templates/statistics.pot +++ b/locales/templates/statistics.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,58 +17,58 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: src/modules/statistics/cog.py:42 +#: src/modules/statistics/cog.py:43 msgctxt "cmd:me" msgid "me" msgstr "" -#: src/modules/statistics/cog.py:45 +#: src/modules/statistics/cog.py:46 msgctxt "cmd:me|desc" msgid "Display your personal profile and summary statistics." msgstr "" -#: src/modules/statistics/cog.py:55 +#: src/modules/statistics/cog.py:56 msgctxt "cmd:stats" msgid "stats" msgstr "" -#: src/modules/statistics/cog.py:58 +#: src/modules/statistics/cog.py:59 msgctxt "cmd:stats|desc" msgid "Weekly and monthly statistics for your recent activity." msgstr "" -#: src/modules/statistics/cog.py:71 +#: src/modules/statistics/cog.py:72 msgctxt "cmd:leaderboard" msgid "leaderboard" msgstr "" -#: src/modules/statistics/cog.py:74 +#: src/modules/statistics/cog.py:75 msgctxt "cmd:leaderboard|desc" msgid "Server leaderboard." msgstr "" -#: src/modules/statistics/cog.py:89 +#: src/modules/statistics/cog.py:90 #, possible-python-brace-format msgctxt "cmd:leaderboard|chunking|desc" msgid "Requesting server member list from Discord, please wait {loading}" msgstr "" -#: src/modules/statistics/cog.py:108 +#: src/modules/statistics/cog.py:113 msgctxt "cmd:configure_statistics" msgid "statistics" msgstr "" -#: src/modules/statistics/cog.py:109 +#: src/modules/statistics/cog.py:114 msgctxt "cmd:configure_statistics|desc" msgid "Statistics configuration panel" msgstr "" -#: src/modules/statistics/cog.py:112 +#: src/modules/statistics/cog.py:117 msgctxt "cmd:configure_statistics|param:season_start" msgid "season_start" msgstr "" -#: src/modules/statistics/cog.py:117 +#: src/modules/statistics/cog.py:122 msgctxt "cmd:configure_statistics|param:season_start|desc" msgid "" "Time from which to start counting activity for rank badges and season " @@ -613,101 +613,108 @@ msgid "" "again to revert." msgstr "" -#: src/modules/statistics/ui/leaderboard.py:250 +#: src/modules/statistics/ui/leaderboard.py:253 msgctxt "ui:leaderboard|menu:stats|placeholder" msgid "Select Activity Type" msgstr "" -#: src/modules/statistics/ui/leaderboard.py:259 +#: src/modules/statistics/ui/leaderboard.py:262 msgctxt "ui:leaderboard|menu:stats|item:voice" msgid "Voice Activity" msgstr "" -#: src/modules/statistics/ui/leaderboard.py:270 +#: src/modules/statistics/ui/leaderboard.py:273 msgctxt "ui:leaderboard|menu:stats|item:study" msgid "Study Statistics" msgstr "" -#: src/modules/statistics/ui/leaderboard.py:281 +#: src/modules/statistics/ui/leaderboard.py:284 msgctxt "ui:leaderboard|menu:stats|item:message" msgid "Message Activity" msgstr "" -#: src/modules/statistics/ui/leaderboard.py:292 +#: src/modules/statistics/ui/leaderboard.py:295 msgctxt "ui:leaderboard|menu;stats|item:anki" msgid "Anki Cards Reviewed" msgstr "" -#: src/modules/statistics/ui/leaderboard.py:346 +#: src/modules/statistics/ui/leaderboard.py:349 msgctxt "ui:leaderboard|button:season|label" msgid "This Season" msgstr "" -#: src/modules/statistics/ui/leaderboard.py:350 +#: src/modules/statistics/ui/leaderboard.py:353 msgctxt "ui:leaderboard|button:day|label" msgid "Today" msgstr "" -#: src/modules/statistics/ui/leaderboard.py:354 +#: src/modules/statistics/ui/leaderboard.py:357 msgctxt "ui:leaderboard|button:week|label" msgid "This Week" msgstr "" -#: src/modules/statistics/ui/leaderboard.py:358 +#: src/modules/statistics/ui/leaderboard.py:361 msgctxt "ui:leaderboard|button:month|label" msgid "This Month" msgstr "" -#: src/modules/statistics/ui/leaderboard.py:362 +#: src/modules/statistics/ui/leaderboard.py:365 msgctxt "ui:leaderboard|button:alltime|label" msgid "All Time" msgstr "" -#: src/modules/statistics/ui/leaderboard.py:366 +#: src/modules/statistics/ui/leaderboard.py:369 msgctxt "ui:leaderboard|button:jump|label" msgid "Jump" msgstr "" -#: src/modules/statistics/ui/leaderboard.py:381 +#: src/modules/statistics/ui/leaderboard.py:384 msgctxt "ui:leaderboard|button:jump|input:title" msgid "Jump to page" msgstr "" -#: src/modules/statistics/ui/leaderboard.py:385 +#: src/modules/statistics/ui/leaderboard.py:388 msgctxt "ui:leaderboard|button:jump|input:question" msgid "Page number to jump to" msgstr "" -#: src/modules/statistics/ui/leaderboard.py:396 +#: src/modules/statistics/ui/leaderboard.py:399 msgctxt "ui:leaderboard|button:jump|error:invalid_page" msgid "Invalid page number, please try again!" msgstr "" -#: src/modules/statistics/ui/leaderboard.py:442 +#: src/modules/statistics/ui/leaderboard.py:443 +msgctxt "ui:leaderboard|chunk_warning" +msgid "" +"**Note:** Could not retrieve member list from Discord, so some members may " +"be missing. Try again in a minute!" +msgstr "" + +#: src/modules/statistics/ui/leaderboard.py:450 #, possible-python-brace-format msgctxt "ui:leaderboard|since" msgid "Counting statistics since {timestamp}" msgstr "" -#: src/modules/statistics/ui/leaderboard.py:453 +#: src/modules/statistics/ui/leaderboard.py:463 #, possible-python-brace-format msgctxt "ui:leaderboard|mode:voice|message:empty|desc" msgid "There has been no voice activity since {timestamp}" msgstr "" -#: src/modules/statistics/ui/leaderboard.py:458 +#: src/modules/statistics/ui/leaderboard.py:468 #, possible-python-brace-format msgctxt "ui:leaderboard|mode:text|message:empty|desc" msgid "There has been no message activity since {timestamp}" msgstr "" -#: src/modules/statistics/ui/leaderboard.py:463 +#: src/modules/statistics/ui/leaderboard.py:473 #, possible-python-brace-format msgctxt "ui:leaderboard|mode:anki|message:empty|desc" msgid "There have been no Anki cards reviewed since {timestamp}" msgstr "" -#: src/modules/statistics/ui/leaderboard.py:472 +#: src/modules/statistics/ui/leaderboard.py:482 msgctxt "ui:leaderboard|message:empty|title" msgid "Leaderboard Empty!" msgstr "" diff --git a/locales/templates/stats-gui.pot b/locales/templates/stats-gui.pot index 8099bcd1..04ab82eb 100644 --- a/locales/templates/stats-gui.pot +++ b/locales/templates/stats-gui.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/locales/templates/sysadmin.pot b/locales/templates/sysadmin.pot index c09da5d5..d6473161 100644 --- a/locales/templates/sysadmin.pot +++ b/locales/templates/sysadmin.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/locales/templates/tasklist.pot b/locales/templates/tasklist.pot index 2190ec80..495fcda4 100644 --- a/locales/templates/tasklist.pot +++ b/locales/templates/tasklist.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/locales/templates/test.pot b/locales/templates/test.pot index 330d6555..3f53ce24 100644 --- a/locales/templates/test.pot +++ b/locales/templates/test.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/locales/templates/text-tracker.pot b/locales/templates/text-tracker.pot index 734e1335..9ac050ae 100644 --- a/locales/templates/text-tracker.pot +++ b/locales/templates/text-tracker.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/locales/templates/timer-gui.pot b/locales/templates/timer-gui.pot index 3a3c1ca8..54611d9c 100644 --- a/locales/templates/timer-gui.pot +++ b/locales/templates/timer-gui.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/locales/templates/user_config.pot b/locales/templates/user_config.pot index e6bc395f..f942e1d4 100644 --- a/locales/templates/user_config.pot +++ b/locales/templates/user_config.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/locales/templates/utils.pot b/locales/templates/utils.pot index 6bca2f4a..9463cb83 100644 --- a/locales/templates/utils.pot +++ b/locales/templates/utils.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/locales/templates/video.pot b/locales/templates/video.pot index 7a90a3ce..0b12151b 100644 --- a/locales/templates/video.pot +++ b/locales/templates/video.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/locales/templates/voice-tracker.pot b/locales/templates/voice-tracker.pot index ace47871..cbe4dbf9 100644 --- a/locales/templates/voice-tracker.pot +++ b/locales/templates/voice-tracker.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/locales/templates/wards.pot b/locales/templates/wards.pot index 4a1694ab..6ee8c2b6 100644 --- a/locales/templates/wards.pot +++ b/locales/templates/wards.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,68 +17,68 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: src/wards.py:79 +#: src/wards.py:83 msgctxt "ward:sys_admin|failed" msgid "You must be a bot owner to do this!" msgstr "" -#: src/wards.py:95 +#: src/wards.py:99 msgctxt "ward:high_management|failed" msgid "You must have the `ADMINISTRATOR` permission in this server to do this!" msgstr "" -#: src/wards.py:111 +#: src/wards.py:115 msgctxt "ward:low_management|failed" msgid "You must have the `MANAGE_GUILD` permission in this server to do this!" msgstr "" -#: src/wards.py:123 +#: src/wards.py:127 msgctxt "ward:moderator|failed" msgid "" "You must have the configured moderator role, or `MANAGE_GUILD` permissions " "to do this." msgstr "" -#: src/wards.py:149 +#: src/wards.py:153 #, possible-python-brace-format msgctxt "ward:equippable_role|error:bot_managed" msgid "I cannot manage {role} because it is managed by another bot!" msgstr "" -#: src/wards.py:156 +#: src/wards.py:160 #, possible-python-brace-format msgctxt "ward:equippable_role|error:integration" msgid "I cannot manage {role} because it is managed by a server integration." msgstr "" -#: src/wards.py:163 +#: src/wards.py:167 msgctxt "ward:equippable_role|error:default_role" msgid "I cannot manage the server's default role." msgstr "" -#: src/wards.py:170 +#: src/wards.py:174 msgctxt "ward:equippable_role|error:no_perms" msgid "I need the `MANAGE_ROLES` permission before I can manage roles!" msgstr "" -#: src/wards.py:177 +#: src/wards.py:181 #, possible-python-brace-format msgctxt "ward:equippable_role|error:my_top_role" msgid "I cannot assign or remove {role} because it is above my top role!" msgstr "" -#: src/wards.py:184 +#: src/wards.py:188 #, possible-python-brace-format msgctxt "ward:equippable_role|error:not_assignable" msgid "I don't have sufficient permissions to assign or remove {role}." msgstr "" -#: src/wards.py:192 +#: src/wards.py:196 msgctxt "ward:equippable_role|error:actor_perms" msgid "You need the `MANAGE_ROLES` permission before you can configure roles!" msgstr "" -#: src/wards.py:199 +#: src/wards.py:203 #, possible-python-brace-format msgctxt "ward:equippable_role|error:actor_top_role" msgid "You cannot configure {role} because it is above your top role!" diff --git a/locales/templates/weekly-gui.pot b/locales/templates/weekly-gui.pot index 6558ba12..779e93be 100644 --- a/locales/templates/weekly-gui.pot +++ b/locales/templates/weekly-gui.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 08:47+0300\n" +"POT-Creation-Date: 2023-09-24 12:21+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n"