Compare commits

6 Commits

9 changed files with 124 additions and 74 deletions

View File

@@ -97,13 +97,12 @@ class PycordBot(LibPycordBot):
continue
if len(event.stage_ids) == 0:
# TODO Make a nice message for management
logger.error("Could not start the event %s: no event stages are defined.", event._id)
await self.notify_admins(
guild,
pycord_guild,
f"Could not start the event **{event.name}**: no event stages are defined.",
self.bot._("admin_event_no_stages_defined", "messages").format(event_name=event.name),
)
await event.cancel(self.cache)
@@ -134,7 +133,9 @@ class PycordBot(LibPycordBot):
await self.notify_admins(
guild,
pycord_guild,
f"Event channel could not be created for user with ID `{user.id}` (<@{user.id}>) and event **{event.name}**: user was not found on the server.",
self.bot._("admin_channel_creation_failed_no_user", "messages").format(
user_id=user.id, event_name=event.name
),
)
continue
@@ -152,7 +153,11 @@ class PycordBot(LibPycordBot):
await self.notify_admins(
guild,
pycord_guild,
f"Event channel could not be created for user **{discord_user.display_name}** ({discord_user.mention}) and event **{event.name}**.",
self.bot._("admin_channel_creation_failed", "messages").format(
display_name=discord_user.display_name,
mention=discord_user.mention,
event_name=event.name,
),
)
continue
@@ -163,10 +168,8 @@ class PycordBot(LibPycordBot):
else File(Path(f"data/{event.thumbnail['id']}"), event.thumbnail["filename"])
)
# Send a notification about event start
# TODO Make a nice message
await user_channel.send(
f"Event **{event.name}** is starting!\n\nUse slash command `/guess` to suggest your answers to each event stage.",
self.bot._("event_is_starting", "messages").format(event_name=event.name),
file=thumbnail,
)
@@ -180,11 +183,10 @@ class PycordBot(LibPycordBot):
chunk, files=None if index != question_chunks_length - 1 else first_stage_files
)
# TODO Make a nice message
await self.notify_admins(
guild,
pycord_guild,
f"Event **{event.name}** has started! Users have gotten their channels and can already start submitting their answers.",
self.bot._("admin_event_started", "messages").format(event_name=event.name),
)
async def _process_events_end(self) -> None:
@@ -218,9 +220,11 @@ class PycordBot(LibPycordBot):
)
continue
# TODO Make a nice message
stages_string: str = "\n\n".join(
f"**Stage {stage.sequence+1}**\nAnswer: ||{stage.answer}||" for stage in stages
self.bot._("stage_entry", "messages").format(
sequence=stage.sequence + 1, answer=stage.answer
)
for stage in stages
)
# Get list of participants
@@ -238,9 +242,8 @@ class PycordBot(LibPycordBot):
# Send a notification about event start
user_channel: TextChannel = guild.get_channel(user.event_channels[str(event._id)])
# TODO Make a nice message
event_ended_string: str = (
f"Event **{event.name}** has ended! Stages and respective answers are listed below.\n\n{stages_string}"
event_ended_string: str = self.bot._("event_ended", "messages").format(
event_name=event.name, stages=stages_string
)
chunk_size: int = 2000
@@ -256,11 +259,10 @@ class PycordBot(LibPycordBot):
# Lock each participant out
await user.lock_event_channel(guild, event._id, channel=user_channel)
# TODO Make a nice message
await self.notify_admins(
guild,
pycord_guild,
f"Event **{event.name}** has ended! Users can no longer submit their answers.",
self.bot._("admin_event_ended", "messages").format(event_name=event.name),
)
await event.end(cache=self.cache)

View File

@@ -17,7 +17,7 @@ logger: Logger = get_logger(__name__)
class PycordGuild:
"""Dataclass of DB entry of a guild"""
__slots__ = ("_id", "id", "channel_id", "category_id", "timezone")
__slots__ = ("_id", "id", "channel_id", "category_id", "timezone", "prefer_emojis")
__short_name__ = "guild"
__collection__ = col_guilds
@@ -26,6 +26,7 @@ class PycordGuild:
channel_id: int | None
category_id: int | None
timezone: str
prefer_emojis: bool
@classmethod
async def from_id(
@@ -145,6 +146,7 @@ class PycordGuild:
"channel_id": self.channel_id,
"category_id": self.category_id,
"timezone": self.timezone,
"prefer_emojis": self.prefer_emojis,
}
# TODO Add documentation
@@ -155,6 +157,7 @@ class PycordGuild:
"channel_id": None,
"category_id": None,
"timezone": "UTC",
"prefer_emojis": False,
}
@staticmethod

View File

@@ -43,7 +43,14 @@ class CogConfig(Cog):
),
required=True,
)
@option("channel", description="Text channel for admin notifications", required=True)
@option(
"channel",
description=_("description", "commands", "config_set", "options", "channel"),
description_localizations=in_every_locale(
"description", "commands", "config_set", "options", "channel"
),
required=True,
)
@option(
"timezone",
description=_("description", "commands", "config_set", "options", "timezone"),
@@ -53,12 +60,21 @@ class CogConfig(Cog):
autocomplete=basic_autocomplete(autocomplete_timezones),
required=True,
)
@option(
"prefer_emojis",
description=_("description", "commands", "config_set", "options", "prefer_emojis"),
description_localizations=in_every_locale(
"description", "commands", "config_set", "options", "prefer_emojis"
),
required=True,
)
async def command_config_set(
self,
ctx: ApplicationContext,
category: CategoryChannel,
channel: TextChannel,
timezone: str,
prefer_emojis: bool,
) -> None:
try:
guild: PycordGuild = await self.bot.find_guild(ctx.guild.id)
@@ -79,6 +95,7 @@ class CogConfig(Cog):
channel_id=channel.id,
category_id=category.id,
timezone=str(timezone_parsed),
prefer_emojis=prefer_emojis,
)
await ctx.respond(self.bot._("config_set", "messages", locale=ctx.locale))
@@ -131,6 +148,7 @@ class CogConfig(Cog):
channel_id=guild.channel_id,
category_id=guild.category_id,
timezone=guild.timezone,
prefer_emojis=guild.prefer_emojis,
)
)

View File

@@ -24,7 +24,6 @@ from modules.utils import (
)
# noinspection Mypy
class CogEvent(Cog):
"""Cog with event management commands."""
@@ -98,10 +97,7 @@ class CogEvent(Cog):
start_date: datetime = datetime.strptime(start, "%d.%m.%Y %H:%M").replace(tzinfo=guild_timezone)
end_date: datetime = datetime.strptime(end, "%d.%m.%Y %H:%M").replace(tzinfo=guild_timezone)
except ValueError:
# TODO Introduce i18n
await ctx.respond(
"Could not parse start and end dates. Please, make sure these are provided in `DD.MM.YYYY HH:MM` format."
)
await ctx.respond(self.bot._("event_dates_parsing_failed", "messages", locale=ctx.locale))
return
if not await validate_event_validity(ctx, name, start_date, end_date, to_utc=True):
@@ -120,9 +116,10 @@ class CogEvent(Cog):
thumbnail=processed_media[0] if thumbnail else None,
)
# TODO Introduce i18n
await ctx.respond(
f"Event **{event.name}** has been created and will take place <t:{get_unix_timestamp(event.starts, to_utc=True)}:R>."
self.bot._("event_created", "messages", locale=ctx.locale).format(
event_name=event.name, start_time=get_unix_timestamp(event.starts, to_utc=True)
)
)
@command_group.command(
@@ -205,10 +202,7 @@ class CogEvent(Cog):
else datetime.strptime(start, "%d.%m.%Y %H:%M").replace(tzinfo=guild_timezone)
)
except ValueError:
# TODO Make a nice message
await ctx.respond(
"Could not parse the start date. Please, make sure it is provided in `DD.MM.YYYY HH:MM` format."
)
await ctx.respond(self.bot._("event_start_date_parsing_failed", "messages", locale=ctx.locale))
return
try:
@@ -218,10 +212,7 @@ class CogEvent(Cog):
else datetime.strptime(end, "%d.%m.%Y %H:%M").replace(tzinfo=guild_timezone)
)
except ValueError:
# TODO Make a nice message
await ctx.respond(
"Could not parse the end date. Please, make sure it is provided in `DD.MM.YYYY HH:MM` format."
)
await ctx.respond(self.bot._("event_end_date_parsing_failed", "messages", locale=ctx.locale))
return
if not await validate_event_validity(
@@ -248,9 +239,11 @@ class CogEvent(Cog):
# TODO Notify participants about time changes
# TODO Make a nice message
await ctx.respond(
f"Event **{pycord_event.name}** has been updated and will take place <t:{get_unix_timestamp(pycord_event.starts, to_utc=True)}:R>."
self.bot._("event_updated", "messages", locale=ctx.locale).format(
event_name=pycord_event.name,
start_time=get_unix_timestamp(pycord_event.starts, to_utc=True),
)
)
@command_group.command(
@@ -309,16 +302,22 @@ class CogEvent(Cog):
or end_date <= datetime.now(tz=ZoneInfo("UTC"))
or start_date <= datetime.now(tz=ZoneInfo("UTC"))
):
# TODO Make a nice message
await ctx.respond("Finished or ongoing events cannot be cancelled.")
await ctx.respond(
self.bot._("event_not_editable", "messages", locale=ctx.locale).format(
event_name=pycord_event.name
)
)
return
await pycord_event.cancel()
# TODO Notify participants about cancellation
# TODO Make a nice message
await ctx.respond(f"Event **{pycord_event.name}** was cancelled.")
await ctx.respond(
self.bot._("event_cancelled", "messages", locale=ctx.locale).format(
event_name=pycord_event.name
)
)
@command_group.command(
name="show",
@@ -346,16 +345,20 @@ class CogEvent(Cog):
stages: List[PycordEventStage] = await self.bot.get_event_stages(pycord_event)
# TODO Make a nice message
stages_string: str = "\n\n".join(
f"**Stage {stage.sequence+1}**\nAnswer: ||{stage.answer}||" for stage in stages
self.bot._("stage_entry", "messages", locale=ctx.locale).format(
sequence=stage.sequence + 1, answer=stage.answer
)
for stage in stages
)
# TODO Show users registered for the event
# TODO Introduce i18n
event_info_string: str = (
f"**Event details**\n\nName: {pycord_event.name}\nStarts: <t:{get_unix_timestamp(starts_date)}>\nEnds: <t:{get_unix_timestamp(ends_date)}>\n\nStages:\n{stages_string}"
event_info_string: str = self.bot._("event_details", "messages", locale=ctx.locale).format(
event_name=pycord_event.name,
start_time=get_unix_timestamp(starts_date),
end_time=get_unix_timestamp(ends_date),
stages=stages_string,
)
chunk_size: int = 2000

View File

@@ -65,9 +65,11 @@ class CogGuess(Cog):
return
if answer.lower() != stage.answer.lower():
# TODO Make a nice message
# await ctx.respond("Provided answer is wrong.")
await ctx.respond(self.bot.config["emojis"]["guess_wrong"])
await ctx.respond(
self.bot.config["emojis"]["guess_wrong"]
if guild.prefer_emojis
else self.bot._("guess_incorrect", "messages", locale=ctx.locale)
)
return
next_stage_index = stage.sequence + 1

View File

@@ -115,8 +115,7 @@ class CogStage(Cog):
media=[] if media is None else processed_media,
)
# TODO Make a nice message
await ctx.respond("Event stage has been created.")
await ctx.respond(self.bot._("stage_created", "messages", locale=ctx.locale))
@command_group.command(
name="edit",
@@ -216,13 +215,11 @@ class CogStage(Cog):
try:
event_stage: PycordEventStage = await self.bot.find_event_stage(stage)
except (InvalidId, EventStageNotFoundError):
# TODO Make a nice message
await ctx.respond("Event stage was not found.")
await ctx.respond(self.bot._("stage_not_found", "messages", locale=ctx.locale))
return
if order is not None and order > len(pycord_event.stage_ids):
# TODO Make a nice message
await ctx.respond("Stage sequence out of range.")
await ctx.respond(self.bot._("stage_sequence_out_of_range", "messages", locale=ctx.locale))
return
processed_media: List[Dict[str, Any]] = (
@@ -239,7 +236,7 @@ class CogStage(Cog):
if order is not None and order - 1 != event_stage.sequence:
await pycord_event.reorder_stage(self.bot, event_stage._id, order - 1, cache=self.bot.cache)
await ctx.respond("Event stage has been updated.")
await ctx.respond(self.bot._("stage_updated", "messages", locale=ctx.locale))
@command_group.command(
name="delete",
@@ -300,15 +297,13 @@ class CogStage(Cog):
try:
event_stage: PycordEventStage = await self.bot.find_event_stage(stage)
except (InvalidId, EventStageNotFoundError):
# TODO Make a nice message
await ctx.respond("Event stage was not found.")
await ctx.respond(self.bot._("stage_not_found", "messages", locale=ctx.locale))
return
await pycord_event.remove_stage(self.bot, event_stage._id, cache=self.bot.cache)
await event_stage.purge(cache=self.bot.cache)
# TODO Make a nice message
await ctx.respond("Event stage has been deleted.")
await ctx.respond(self.bot._("stage_deleted", "messages", locale=ctx.locale))
def setup(bot: PycordBot) -> None:

View File

@@ -1,14 +1,36 @@
{
"messages": {
"admin_channel_creation_failed": "Event channel could not be created for user **{display_name}** ({mention}) and event **{event_name}**.",
"admin_channel_creation_failed_no_user": "Event channel could not be created for user with ID `{user_id}` (<@{user_id}>) and event **{event_name}**: user was not found on the server.",
"admin_event_ended": "Event **{event_name}** has ended! Users can no longer submit their answers.",
"admin_event_no_stages_defined": "Could not start the event **{event_name}**: no event stages are defined.",
"admin_event_started": "Event **{event_name}** has started! Users have gotten their channels and can already start submitting their answers.",
"admin_user_channel_creation_failed": "Event channel could not be created for user **{display_name}** ({mention}) and event **{event_name}**.",
"admin_user_channel_fixed": "Fixed event channel of user **{display_name}** ({mention}) for the event **{event_name}**.",
"admin_user_completed_event": "User **{display_name}** ({mention}) has completed the event **{event_name}**",
"admin_user_completed_stage": "User **{display_name}** ({mention}) has completed the stage {stage_sequence} of the event **{event_name}**.",
"admin_user_channel_fixed": "Fixed event channel of user **{display_name}** ({mention}) for the event **{event_name}**.",
"config_reset": "Configuration has been reset. You can update it using `/config set`, otherwise no events can be held.",
"config_set": "Configuration has been updated. You can review it anytime using `/config show`.",
"config_show": "**Guild config**\n\nChannel: <#{channel_id}>\nCategory: <#{category_id}>\nTimezone: `{timezone}`",
"config_show": "**Guild config**\n\nChannel: <#{channel_id}>\nCategory: <#{category_id}>\nTimezone: `{timezone}`\nPrefer emojis: `{prefer_emojis}`",
"event_cancelled": "Event **{event_name}** was cancelled.",
"event_created": "Event **{event_name}** has been created and will take place <t:{start_time}:R>.",
"event_dates_parsing_failed": "Could not parse start and end dates. Please, make sure these are provided in `DD.MM.YYYY HH:MM` format.",
"event_details": "**Event details**\n\nName: {event_name}\nStarts: <t:{start_time}>\nEnds: <t:{end_time}>\n\nStages:\n{stages}",
"event_end_before_start": "Start date must be before end date",
"event_end_date_parsing_failed": "Could not parse the end date. Please, make sure it is provided in `DD.MM.YYYY HH:MM` format.",
"event_end_past": "End date must not be in the past",
"event_ended": "Event **{event_name}** has ended! Stages and respective answers are listed below.\n\n{stages}",
"event_is_cancelled": "This event was cancelled.",
"event_is_starting": "Event **{event_name}** is starting!\n\nUse slash command `/guess` to suggest your answers to each event stage.",
"event_name_duplicate": "There can only be one active event with the same name",
"event_not_editable": "Finished or ongoing events cannot be cancelled.",
"event_not_found": "Event was not found.",
"event_ongoing_not_editable": "Ongoing events cannot be modified.",
"event_start_date_parsing_failed": "Could not parse the start date. Please, make sure it is provided in `DD.MM.YYYY HH:MM` format.",
"event_start_past": "Start date must not be in the past",
"event_updated": "Event **{event_name}** has been updated and will take place <t:{start_time}:R>.",
"guess_completed_event": "Congratulations! You have completed the event!",
"guess_incorrect": "Provided answer is wrong.",
"guess_incorrect_channel": "Usage outside own event channel is not allowed.",
"guess_incorrect_event": "Your event could not be found. Please, contact the administrator.",
"guess_unregistered": "You have no ongoing events. You can register for events using the `/register` command.",
@@ -20,17 +42,23 @@
"register_already_registered": "You are already registered for this event.",
"register_success_ongoing": "You are now registered for the event **{event_name}**.\n\nNew channel has been created for you and further instructions will are provided in it. Good luck!",
"register_success_scheduled": "You are now registered for the event **{event_name}**.\n\nNew channel will be created for you and further instructions will be provided as soon as the event starts <t:{event_starts}:R>. Good luck!",
"stage_created": "Event stage has been created.",
"stage_deleted": "Event stage has been deleted.",
"stage_entry": "**Stage {sequence}**\nAnswer: ||{answer}||",
"stage_not_found": "Event stage was not found.",
"stage_sequence_out_of_range": "Stage sequence out of range.",
"stage_updated": "Event stage has been updated.",
"status": "**QuizBot** v{version}\n\nUptime: since <t:{start_time}>",
"status_git": "**QuizBot** v{version} (`{commit}`)\n\nUptime: up since <t:{start_time}>",
"timezone_invalid": "Timezone **{timezone}** was not found. Please, select one of the timezones provided by the autocompletion.",
"unexpected_error": "An unexpected error has occurred. Please, contact the administrator.",
"unregister_not_registered": "You are not registered for this event.",
"unregister_unregistered": "You are no longer registered for this event.",
"user_channels_updated": "Event channels of the user **{display_name}** were updated.",
"user_jail_already_jailed": "User **{display_name}** is already jailed.",
"user_jail_successful": "User **{display_name}** has been jailed and cannot interact with events anymore.",
"user_unjail_not_jailed": "User **{display_name}** is not jailed.",
"user_unjail_successful": "User **{display_name}** has been unjailed and can interact with events again.",
"user_channels_updated": "Event channels of the user **{display_name}** were updated."
"user_unjail_successful": "User **{display_name}** has been unjailed and can interact with events again."
},
"commands": {
"config": {
@@ -47,6 +75,9 @@
},
"timezone": {
"description": "Timezone in which events take place"
},
"prefer_emojis": {
"description": "Prefer emojis over text messages where available"
}
}
},

View File

@@ -6,6 +6,7 @@ from bson import ObjectId
from discord import (
ApplicationContext,
)
from libbot.i18n import _
from modules.database import col_events
@@ -22,18 +23,15 @@ async def validate_event_validity(
end_date_internal: datetime = end_date.astimezone(ZoneInfo("UTC")) if to_utc else end_date
if start_date_internal < datetime.now(tz=ZoneInfo("UTC")):
# TODO Make a nice message
await ctx.respond("Start date must not be in the past")
await ctx.respond(_("event_start_past", "messages", locale=ctx.locale))
return False
if end_date_internal < datetime.now(tz=ZoneInfo("UTC")):
# TODO Make a nice message
await ctx.respond("End date must not be in the past")
await ctx.respond(_("event_end_past", "messages", locale=ctx.locale))
return False
if start_date_internal >= end_date_internal:
# TODO Make a nice message
await ctx.respond("Start date must be before end date")
await ctx.respond(_("event_end_before_start", "messages", locale=ctx.locale))
return False
# TODO Add validation for concurrent events.
@@ -49,8 +47,7 @@ async def validate_event_validity(
query["_id"] = {"$ne": event_id}
if (await col_events.find_one(query)) is not None:
# TODO Make a nice message
await ctx.respond("There can only be one active event with the same name")
await ctx.respond(_("event_name_duplicate", "messages", locale=ctx.locale))
return False
return True

View File

@@ -2,6 +2,7 @@ from datetime import datetime
from zoneinfo import ZoneInfo
from discord import ApplicationContext
from libbot.i18n import _
async def is_operation_confirmed(ctx: ApplicationContext, confirm: bool) -> bool:
@@ -17,8 +18,7 @@ async def is_event_status_valid(
event: "PycordEvent",
) -> bool:
if event.is_cancelled:
# TODO Make a nice message
await ctx.respond("This event was cancelled.")
await ctx.respond(_("event_is_cancelled", "messages", locale=ctx.locale))
return False
if (
@@ -26,8 +26,7 @@ async def is_event_status_valid(
<= datetime.now(tz=ZoneInfo("UTC"))
<= event.ends.replace(tzinfo=ZoneInfo("UTC"))
):
# TODO Make a nice message
await ctx.respond("Ongoing events cannot be modified.")
await ctx.respond(_("event_ongoing_not_editable", "messages", locale=ctx.locale))
return False
return True