diff --git a/README.md b/README.md index 8e66250b..ea0dac98 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ZBot V4 - A Discord Bot +# Axobot V4 - A Discord Bot [![discord.py](https://img.shields.io/badge/Discord.py-2.0-brightgreen.svg)](https://github.com/Rapptz/discord.py) [![Docs status](https://readthedocs.org/projects/zbot/badge/?version=latest)](https://zbot.readthedocs.io) @@ -8,15 +8,15 @@ [![GitHub Super-Linter](https://github.com/ZRunner/ZBot/workflows/Lint%20Code%20Base/badge.svg)](https://github.com/marketplace/actions/super-linter) [![Discord server](https://discord.com/api/v8/guilds/356067272730607628/widget.png)](https://discord.gg/N55zY88) -[![Discord bot](https://top.gg/api/widget/status/486896267788812288.svg)](https://zrunner.me/invitezbot) +[![Discord bot](https://top.gg/api/widget/status/486896267788812288.svg)](https://zrunner.me/invite-axobot) -Welcome to the ZBot bot documentation, a Discord bot coded in Python 3 by a small French developer, **Z_runner**. You will find in the documentation all the explanations on each of the usable commands, as well as the list of the last additions and some code examples. The documentation is currently in English but the bot is available in several languages, including its main language, French. +Welcome to the Axobot (previously ZBot) bot documentation, a Discord bot coded in Python 3 by a small French developer, **Z_runner**. You will find in the documentation all the explanations on each of the usable commands, as well as the list of the last additions and some code examples. The documentation is currently in English but the bot is available in several languages, including its main language, French. Full documentation can be found on the [ReadTheDocs](https://zbot.readthedocs.io/en/latest/) page ## The Bot -ZBot is a multifunction Discord bot, which allows among other things to : +Axobot is a multifunction Discord bot, which allows among other things to : - moderate a server - get information about a member/role/channel... diff --git a/assets/card-models/.gitkeep b/assets/card-models/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/assets/cards/.gitkeep b/assets/cards/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/assets/images/.gitkeep b/assets/images/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/docs/Makefile b/docs/Makefile index ebf9dddf..b9c8725a 100755 --- a/docs/Makefile +++ b/docs/Makefile @@ -4,7 +4,7 @@ # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build -SPHINXPROJ = Zbot +SPHINXPROJ = Axobot SOURCEDIR = . BUILDDIR = _build diff --git a/docs/conf.py b/docs/conf.py index 8db4ff76..c85c53fe 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,14 +19,14 @@ # -- Project information ----------------------------------------------------- -project = 'ZBot' +project = 'Axobot' copyright = '2019 - 2022, ZRunner' author = 'ZRunner' # The short X.Y version -version = '4.1' +version = '4.2' # The full version, including alpha/beta/rc tags -release = '4.1.4' +release = '4.2.0' # Example configuration for intersphinx: refer to the Python standard library. diff --git a/docs/fun.rst b/docs/fun.rst index c4e8e3e0..9b989c8f 100644 --- a/docs/fun.rst +++ b/docs/fun.rst @@ -227,7 +227,7 @@ Any of you like pizza here? Those beautiful dripping pieces of melted cheese and Pong ---- -**Syntax:** :code:`ping` +**Syntax:** :code:`pong` This is probably the most useless command in the bot. Try it, you may (maybe) not be disappointed! diff --git a/docs/index.rst b/docs/index.rst index 8f33a75e..d2c780d8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -31,6 +31,7 @@ Translations of the Zbot `Privacy Policy `_ and its `Terms o minecraft rss roles-reactions + streamers fun miscellaneous diff --git a/docs/moderator.rst b/docs/moderator.rst index 9cb9d91e..08e455c9 100644 --- a/docs/moderator.rst +++ b/docs/moderator.rst @@ -65,15 +65,13 @@ Clear **Syntax:** :code:`clear [parameters]` -This command allows you to efficiently delete messages, with a list of possible parameters for more accuracy. You can thus specify a list of members to check by mentioning them, `+i` to delete all messages containing files/images, `+l` for those containing links or Discord invitations, `+p` for pinned messages. By default, the bot will not delete pinned messages. +This command allows you to efficiently delete messages, with a list of possible parameters for more accuracy. You can thus specify a list of members to check by mentioning them, if it should delete all messages containing files/images, or all containing links or Discord invitations, or even pinned messages. By default, the bot will not delete pinned messages. -Be careful, all specified settings must be validated for the message to be deleted. For example, if you enter :code:`clear 10 @Z_runner#7515 +i`, the bot will check in the last ten messages if the message comes from Z_runner#7515 AND if the message contains an image. +Be careful, all specified settings must be validated for the message to be deleted. For example, if you enter :code:`/clear 10 users: @Z_runner#7515 contains_file: True`, the bot will check in the last ten messages if the message comes from Z_runner#7515 AND if the message contains an image. -If you enter :code:`clear 25 -p +l`, the bot will clear the last 25 messages if they contains a link AND if they're not pinned, no matter the author. +If you enter :code:`/clear 25 contains_url: True`, the bot will clear the last 25 messages if they contains a link AND if they're not pinned, no matter the author. -If you enter :code:`clear 13 -p -i @Z_runner#7515`, the bot will clear the last 13 messages if they are not pinned AND if they does not contain any file/image AND if the author is Z_runner#7515. - -If you enter :code:`clear 1000 @Z_runner#7515 @ZBot beta#4940`, the bot will delete all messages contained in the last 1000 messages of the channel AND written by Z_runner#7515 OR ZBot beta#4940 +If you enter :code:`/clear 1000 users: @Z_runner#7515 @ZBot beta#4940`, the bot will delete all (not pinned) messages contained in the last 1000 messages of the channel AND written by Z_runner#7515 OR ZBot beta#4940 .. warning:: The permissions "`Manage messages `__" and "`Read messages history `__" are required. @@ -233,7 +231,7 @@ To help you moderate your server and keep track of what's going on, Zbot has a l How to setup logs ----------------- -You can enable one or more logs types in a channel by using the :code:`modlogs enable ` in the channel you want them to appear in. In the same way, use :code:`modlogs disable ` to disable a kind of logs in the current channel. Please note that you can use the keyword "all" as a log type to enable or disable all at the same time. +You can enable one or more logs types in a channel by using the :code:`modlogs enable [channel]` in the channel you want them to appear in (or in another channel by mentionning it). In the same way, use :code:`modlogs disable [channel]` to disable a kind of logs in the current channel. Please note that you can use the keyword "all" as a log type to enable or disable all at the same time. To see in Discord which logs exists and which ones you have enabled in your server, use the command :code:`modlogs list`. You can also use this command followed by a channel mention or ID to see which logs are enabled in a specific channel. diff --git a/docs/perms.rst b/docs/perms.rst index cf1c032f..441722a5 100644 --- a/docs/perms.rst +++ b/docs/perms.rst @@ -6,6 +6,7 @@ The permissions given to members is an important part in the configuration of a .. warning:: Never *never* **never NEVER** never *(yes, 5 times never)* put a bot with administration permissions. It has already happened once that the bot's security key is stolen, which allows the thief to take full control of the bot, such as deleting your channels or banning your members. Even though safety has been completely redesigned since this incident, zero risk is not possible. See `this official note from Discord `__ for more information. + ------------------- General Permissions ------------------- @@ -15,78 +16,76 @@ Administrator Grant every possible permission in the server. Someone with this permission will not have any restriction, except deleting the server and editing the roles above them. Not recommended to anyone, even a bot. - View Audit Log -------------- Allows the bot to read server logs (adding roles, changing names, editing channels...). Not necessary for the moment - Manage Server ------------- Allows the bot to change the name, image and region of the server, or get the list of all invites. Used for: `invite `__ - Manage Roles ------------ Allows the bot to create and delete roles, or edit the permissions of roles lower than his own, and to give them to other members. Examples of use: `mute `__, `voice roles `__ - Manage Channels --------------- Allows the bot to create, delete and modify channels (create invitations for example). Examples of use: `membercounter option `__, `voice channels automation `__ - Kick Members ------------ Allows the bot to eject a member from the server. Examples of use: `kick `__ `anti-raid system `__ - Ban Members ----------- Allows the bot to ban or unban a member from the server, as well as to consult the list of banned members. Examples of use: `ban `__ , `unban `__, `banlist `__, `softban `__ +Time out Members +---------------- + +Allows the bot to temporarily mute a member, preventing them to send messages, add reactions, and speak in voice channels. Examples of use: `mute `__ Create Invite ------------- Allows the bot to create invitations to any visible room, without being able to modify or delete them. Not used. - Change Nickname --------------- Allows the bot to change your own nickname. Not used at this moment. - Manage Nickname --------------- Allows the bot to change the nickname of any member hierarchically equal or inferior to you. Example of use: `unhoist command `__ +Manage Emojis / Manage Stickers +------------------------------- -Manage Emojis -------------- - -Allows the bot to add, rename or delete emojis from the server. Example of use: `emoji `__ - +Allows the bot to add, rename or delete emojis and stickets from the server. Example of use: `emoji `__ Manage Webhooks --------------- Allows the bot to read, add, modify or delete `webhooks `__ . Example of use: `infos `__ - Read Text Channels & See Voice Channels --------------------------------------- Allows the bot to see chats and voice channels. This permission does not allow you to write in these chats or connect to the voice channels. Required for the bot. +Manage Events +------------- + +Allows to create, edit and cancel server events. Not used at this moment. + ---------------- Text Permissions @@ -98,64 +97,71 @@ Read Messages/See channel Allows the bot to read messages from a chat, but not the history. In other words, the bot will react to your messages but will not be able to read them again. Remove this permission in a channel to prevent the bot from being there. -Send Messages -------------- - -Allows the bot you to write messages in text channels. Required for almost all functionalities, but not necessarily for all channels. +Send Messages / Send Messages in Threads +---------------------------------------- +Allows the bot you to write messages in text channels and threads. Required for almost all functionalities, but not necessarily for all channels. -Send TTS Messages ------------------ - -Allows the bot to send a TTS message, i.e. a message that will be read aloud by your application. No need for the bot. +Create Public/Private Threads +----------------------------- -Manage Messages ---------------- +Allows the bot to create public or private threads in text channels. Required for the `tickets system `__ when configured to create threads. -Allows the bot to pin or delete any message. Examples of use: `mute `__ , `freeze `__ , `clear `__ , `purge `__ , `fun commands `__ +Send TTS Messages +----------------- +Allows the bot to send a TTS (text-to-speech) message, i.e. a message that will be read aloud by your application. No need for the bot. Embed Links ----------- Allows the bot the bot to send an embed. Some commands will need that permissions, some others will only look worse. Examples of use for a better display: `membercount `__ , `mojang `__, `XP system `__ . Examples of required permission: `infos `__ , `mc `__ , `config see `__, `embeds generator `__ - Attach Files ------------ Allows the bot to send files (such as images) in a channel. Examples of use: `fun commands `__, `XP cards `__ - Read Message History -------------------- Allows the bot to read the history of all messages in a channel. Examples of use: `clear `__ , `purge `__ , `some fun commands `__ - -Mention @veryone, @here and @All Roles +Mention @veryone, @here and All Roles -------------------------------------- Allows the bot to mention any role *including* @everyone (which results in sending a notification to all members with access to the channel) and @here (sends a notification to all online members with access to the channel). Zbot uses a great Discord protection to avoid unwanted mentions, so you should be safe granting it. Example of use: `rss follows with mentions `__ - Use External Emojis ------------------- Allows the bot to use emojis from any other server. The bot uses them in many situations to diversify emotions, so it is strongly recommended to keep it activated. +Use External Stickers +--------------------- + +Allows the bot to use stickers from any other server. Bots cannot use stickers atm, so this permission has no effect. + +Manage Messages +--------------- + +Allows the bot to pin or delete any message. Examples of use: `mute `__ , `freeze `__ , `clear `__ , `purge `__ , `fun commands `__ + +Manage Threads +-------------- + +Allows the bot to edit and delete threads. Not used at this moment. Add Reactions ------------- Allows the bot you to add reactions to a message, whether they are Discord or server emotions. Examples of use: `react `__, `vote command `__, `poll channels `__ +Use Application Commands +------------------------ -Use Slash Commands ------------------- - -Allows the user to use bots slash commands. Obviously has no effect on bots. +Allows the user to use bots commands (ie. slash commands as well as user and message context commands). Obviously has use for bots. ----------------- @@ -202,7 +208,6 @@ Priority Speaker Allows users to have their volume higher than the other members in a voice channel. Bots cannot use that for now. - Request To Speak ---------------- diff --git a/docs/streamers.rst b/docs/streamers.rst new file mode 100644 index 00000000..8ba721bb --- /dev/null +++ b/docs/streamers.rst @@ -0,0 +1,58 @@ +:og:description: Zbot has a super cool streamers system allowing you to follow your favorite Twitch streamers right inside your server, and give your own streamers a special role when they're live! + +.. note:: Like most of the features of this bot, this streamers subscription system is constantly being developed. Feel free to help us by offering suggestions, voting for the best ideas or reporting bugs at our `Discord server `__! + +.. warning:: All of these setup commands are reserved for certain roles only: you need the "Manage server" (or administrator) permission if you want to use them! + +====================== +Streamers subscription +====================== + +It is common for Discord communities to grow around one or a few streamers and focus their activities around these popular people. That's why Zbot offers you a simple and efficient system to follow your chosen streamers closely, and be notified when they go live. +Moreover, if you have streamers in your community, you can assign them a special role when they are live, for example to highlight them or give them special permissions just for the time of the live! + +The role you choose to give to streamers will only be given to members who have an active stream on one of the channels the server is subscribed to. Also, due to technical limitations, these streamers must have streaming activity visible on their Discord profile to receive the role (not happy with that? `Let us know `__!) + + +Configure your server +--------------------- + +**Syntax:** :code:`config change streaming_channel ` + +This command will set the channel where Zbot will send notifications when a streamer goes live. You can enter both the channel mention, its name, and its ID. Make sure Zbot can send messages and embeds there! + +**Syntax:** :code:`config change streaming_role ` + +This command will set the role that will be given to streamers when they go live. You can enter both the role mention, its name, and its ID. Make sure Zbot role is higher than this role and has the "Manage roles" permission! + +**Syntax:** :code:`config change stream_mention ` + +This command will set the role that will be mentioned when a streamer goes live. You can enter both the role mention, its name, and its ID. Make sure Zbot can mention it! + + +Subscribe or unsubscribe to a streamer +-------------------------------------- + +**Syntax:** :code:`twitch subscribe ` + +Subscribe your server to up to 20 Twitch channels with this command. You can enter both the channel name and its twitch.tv URL. Zbot will let you know if you are already subscribed to this channel or if you have reached your subscription limit + +**Syntax:** :code:`twitch unsubscribe ` + +Unsubscribe your server from a Twitch channel. You have to enter its channel name, but slash command autocompletion will help you quickly finding it! + + +List your subscriptions +----------------------- + +**Syntax:** :code:`twitch list-subscriptions` + +This command will list all the channels your server is subscribed to, with a small notice for those that are currently live. Zbot requires the "Embed messages" permission to send the list. + + +Check a streamer status +----------------------- + +**Syntax:** :code:`twitch check-stream ` + +This command will check if a streamer is currently live, and if so, will display some information about the stream. Zbot requires the "Embed messages" permission to send the message if a live is ongoing. diff --git a/fcts/admin.py b/fcts/admin.py index 33b3482e..fe02321c 100644 --- a/fcts/admin.py +++ b/fcts/admin.py @@ -13,8 +13,9 @@ import discord import speedtest from cachingutils import acached +from git import Repo, exc from discord.ext import commands -from libs.bot_classes import PRIVATE_GUILD_ID, MyContext, Zbot +from libs.bot_classes import PRIVATE_GUILD_ID, SUPPORT_GUILD_ID, MyContext, Zbot from libs.enums import RankCardsFlag, UserFlag from libs.views import ConfirmView @@ -30,7 +31,7 @@ def cleanup_code(content: str): return content.strip('` \n') class Admin(commands.Cog): - """Here are listed all commands related to the internal administration of the bot. Most of them are not accessible to users, but only to ZBot administrators.""" + """Here are listed all commands related to the internal administration of the bot. Most of them are not accessible to users, but only to the bot administrators.""" def __init__(self, bot: Zbot): self.bot = bot @@ -89,22 +90,12 @@ async def add_success_reaction(self, msg: discord.Message): except discord.DiscordException: pass - @discord.app_commands.command() - @discord.app_commands.guilds(PRIVATE_GUILD_ID) - @discord.app_commands.default_permissions(administrator=True) - @discord.app_commands.check(checks.is_bot_admin) - async def send_msg(self, interaction: discord.Interaction, user: discord.User, message: str): - "Send a DM to any user the bot can reach" - await interaction.response.defer(ephemeral=True) - await user.send(message) - await interaction.edit_original_response(content="Done!") - @commands.hybrid_group(name='admin', hidden=True) @discord.app_commands.guilds(PRIVATE_GUILD_ID) @discord.app_commands.default_permissions(administrator=True) @commands.check(checks.is_bot_admin) async def main_msg(self, ctx: MyContext): - """Commandes réservées aux administrateurs de ZBot""" + """Commandes réservées aux administrateurs du bot""" if ctx.subcommand_passed is None: text = "Liste des commandes disponibles :" for cmd in sorted(ctx.command.commands, key=lambda x:x.name): @@ -114,23 +105,37 @@ async def main_msg(self, ctx: MyContext): text+="\n - {} *({})*".format(cmds.name,cmds.help.split('\n')[0]) await ctx.send(text) + @main_msg.command(name="send-msg") + @discord.app_commands.check(checks.is_bot_admin) + async def send_msg(self, ctx: MyContext, user: discord.User, message: str): + "Send a DM to any user the bot can reach" + await ctx.defer() + await user.send(message) + await ctx.send(content="Done!") + @main_msg.command(name="sync") @commands.check(checks.is_bot_admin) async def sync_app_commands(self, ctx: MyContext, scope: typing.Literal["global", "staff-guild", "support-guild"]): "Sync app commands for either global or staff server scope" await ctx.defer() if scope == "global": - cmds = await self.bot.tree.sync() - txt = f"{len(cmds)} global app commands synced" + if self.bot.beta: + self.bot.tree.copy_global_to(guild=PRIVATE_GUILD_ID) + cmds = await self.bot.tree.sync(guild=PRIVATE_GUILD_ID) + txt = f"{len(cmds)} (global + local) app commands synced in staff server" + else: + cmds = await self.bot.tree.sync() + txt = f"{len(cmds)} global app commands synced" elif scope == "staff-guild": - cmds = await self.bot.tree.sync(guild=discord.Object(id=625316773771608074)) - txt = f"{len(cmds)} global app commands synced in staff server" + cmds = await self.bot.tree.sync(guild=PRIVATE_GUILD_ID) + txt = f"{len(cmds)} app commands synced in staff server" elif scope == "support-guild": - cmds = await self.bot.tree.sync(guild=discord.Object(id=356067272730607628)) - txt = f"{len(cmds)} global app commands synced in the support server" + cmds = await self.bot.tree.sync(guild=SUPPORT_GUILD_ID) + txt = f"{len(cmds)} app commands synced in the support server" else: await ctx.send("Unknown scope") return + self.bot.app_commands_list = None self.bot.log.info(txt) emb = discord.Embed(description=txt, color=discord.Color.blue()) await self.bot.send_embed(emb) @@ -236,6 +241,9 @@ async def send_updates(self, ctx:MyContext): return count = 0 for guild in ctx.bot.guilds: + if guild.id == 356067272730607628 and self.bot.entity_id == 0: + # The support server should not receive updates from Zbot but only Axobot + continue channels = await ctx.bot.get_config(guild.id,'bot_news') if channels is None or len(channels) == 0: continue @@ -329,15 +337,38 @@ async def restart_bot(self, ctx: MyContext): self.bot.log.info("Redémarrage du bot") os.execl(sys.executable, sys.executable, *args) + @main_msg.command(name="pull") + @commands.check(checks.is_bot_admin) + async def git_pull(self, ctx: MyContext, branch: typing.Optional[typing.Literal["main", "develop", "release-candidate"]]=None, install_requirements: bool=False): + """Pull du code depuis le dépôt git""" + msg = await ctx.send("Pull en cours...") + repo = Repo(os.getcwd()) + assert not repo.bare + if branch: + try: + repo.git.checkout(branch) + except exc.GitCommandError as e: + self.bot.dispatch("command_error", ctx, e) + else: + msg = await msg.edit(content=msg.content+f"\nBranche {branch} correctement sélectionnée") + origin = repo.remotes.origin + origin.pull() + msg = await msg.edit(content=msg.content + f"\nPull effectué avec succès sur la branche {repo.active_branch.name}") + if install_requirements: + await msg.edit(content=msg.content+"\nInstallation des dépendances...") + os.system("pip install -qr requirements.txt") + msg = await msg.edit(content=msg.content+"\nDépendances installées") + @main_msg.command(name="reload") @commands.check(checks.is_bot_admin) async def reload_cog(self, ctx: MyContext, *, cog: str): """Recharge un module""" cogs = cog.split(" ") await self.bot.get_cog("Reloads").reload_cogs(ctx,cogs) - + @reload_cog.autocomplete("cog") async def reload_cog_autocom(self, interaction: discord.Interaction, current: str): + "Autocompletion for the cog name" if " " in current: fixed, current = current.rsplit(" ", maxsplit=1) else: @@ -383,7 +414,7 @@ async def admin_sconfig_see(self, ctx: MyContext, guild: discord.Guild, option=N if not ctx.bot.database_online: await ctx.send("Impossible d'afficher cette commande, la base de donnée est hors ligne :confused:") return - await self.bot.get_cog("Servers").send_see(guild, ctx.channel, option, ctx.message, guild) + await self.bot.get_cog("Servers").send_see(guild, ctx, option, ctx.message) @main_msg.group(name="database", aliases=["db"]) @commands.check(checks.is_bot_admin) @@ -394,6 +425,7 @@ async def admin_db(self, _ctx: MyContext): @commands.check(checks.is_bot_admin) async def db_reload(self, ctx: MyContext): "Reconnecte le bot à la base de donnée" + await ctx.defer() self.bot.cnx_frm.close() self.bot.connect_database_frm() self.bot.cnx_xp.close() @@ -405,6 +437,8 @@ async def db_reload(self, ctx: MyContext): await self.add_success_reaction(ctx.message) if xp := self.bot.get_cog("Xp"): await xp.reload_sus() + if servers := self.bot.get_cog("Servers"): + await servers.clear_cache() @admin_db.command(name="biggest-tables") @commands.check(checks.is_bot_admin) @@ -421,10 +455,10 @@ async def db_biggest(self, ctx: MyContext, database: typing.Optional[str] = None length = max(len(result[0]) for result in query_results) txt = "\n".join(f"{result[0]:>{length}}: {result[1]} MB" for result in query_results if result[1] is not None) await ctx.send("```yaml\n" + txt + "\n```") - + @acached(timeout=3600) async def get_databases_names(self) -> list[str]: - "Get every database names visible for Zbot" + "Get every database names visible for the bot" query = "SHOW DATABASES" async with self.bot.db_query(query, astuple=True) as query_results: print(query_results) @@ -432,6 +466,7 @@ async def get_databases_names(self) -> list[str]: @db_biggest.autocomplete("database") async def db_biggest_autocompl(self, interaction: discord.Interaction, current: str): + "Autocompletion for the database name" databases = await self.get_databases_names() return [ discord.app_commands.Choice(name=db, value=db) @@ -458,7 +493,7 @@ async def emergency(self, level=100): msg = await user.dm_channel.send("{} La procédure d'urgence vient d'être activée. Si vous souhaitez l'annuler, veuillez cliquer sur la réaction ci-dessous dans les {} secondes qui suivent l'envoi de ce message.".format(self.bot.emojis_manager.customs['red_warning'], time)) await msg.add_reaction('🛑') except Exception as err: - await self.bot.get_cog('Errors').on_error(err, "Emergency command") + self.bot.dispatch("error", err, "Emergency command") def check(_, user: discord.User): return user.id in checks.admins_id @@ -486,7 +521,7 @@ def check(_, user: discord.User): user = self.bot.get_user(x) await user.send("La procédure a été annulée !") except Exception as err: - await self.bot.get_cog('Errors').on_error(err,None) + self.bot.dispatch("error", err, None) return "Qui a appuyé sur le bouton rouge ? :thinking:" @main_msg.command(name="ignore") @@ -588,7 +623,7 @@ async def enable_module(self, ctx: MyContext, module: typing.Literal["xp", "rss" async def admin_flag(self, ctx: MyContext): "Ajoute ou retire un attribut à un utilisateur" if ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx, ['admin', 'flag']) + await ctx.send_help(ctx.command) @admin_flag.command(name="list") @commands.check(checks.is_bot_admin) @@ -644,7 +679,7 @@ async def admin_flag_remove(self, ctx: MyContext, user: discord.User, flag: str) async def admin_rankcard(self, ctx: MyContext): "Ajoute ou retire une carte d'xp à un utilisateur" if ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx, ['admin', 'rankcard']) + await ctx.send_help(ctx.command) @admin_rankcard.command(name="list") @commands.check(checks.is_bot_admin) @@ -718,7 +753,7 @@ async def owner_reload(self, ctx: MyContext): Il est nécessaire d'avoir au moins 10 membres pour que le rôle soit ajouté""" server = self.bot.get_guild(356067272730607628) if server is None: - await ctx.send("Serveur ZBot introuvable") + await ctx.send("Serveur de support introuvable") return role = server.get_role(486905171738361876) if role is None: @@ -811,7 +846,8 @@ async def change_activity(self, ctx: MyContext, activity_type: typing.Literal["p await self.bot.change_presence(activity=discord.Activity(type=discord.ActivityType.streaming,name=text, timestamps={'start':time.time()})) else: await ctx.send("Sélectionnez *play*, *watch*, *listen* ou *stream* suivi du nom") - await ctx.message.delete() + if not ctx.interaction: + await ctx.message.delete() @main_msg.command(name="speedtest") @commands.check(checks.is_bot_admin) @@ -843,7 +879,7 @@ async def speedtest(self, ctx: MyContext, method: typing.Literal["dict", "csv", await msg.edit(content=str(result)) - @commands.command(name='eval') + @commands.command(name='eval', hidden=True) @commands.check(checks.is_bot_admin) async def _eval(self, ctx: MyContext, *, body: str): """Evaluates a code @@ -863,19 +899,19 @@ async def _eval(self, ctx: MyContext, *, body: str): stdout = io.StringIO() try: to_compile = f'async def func():\n{textwrap.indent(body, " ")}' - except Exception as e: - await self.bot.get_cog('Errors').on_error(e,ctx) + except Exception as err: + self.bot.dispatch("error", err, ctx) return try: exec(to_compile, env) # pylint: disable=exec-used - except Exception as e: - return await ctx.send(f'```py\n{e.__class__.__name__}: {e}\n```') + except Exception as err: + return await ctx.send(f'```py\n{err.__class__.__name__}: {err}\n```') func = env['func'] try: with redirect_stdout(stdout): ret = await func() - except Exception as e: + except Exception as err: value = stdout.getvalue() await ctx.send(f'```py\n{value}{traceback.format_exc()[:1990]}\n```') else: diff --git a/fcts/aide.py b/fcts/aide.py index 2386e4bd..d700d137 100644 --- a/fcts/aide.py +++ b/fcts/aide.py @@ -1,12 +1,15 @@ import copy import inspect import json -from typing import List +from typing import List, Optional, TypedDict import discord from discord.ext import commands from libs.bot_classes import MyContext, Zbot +class CommandsCategoryData(TypedDict): + emoji: str + commands: list[str] class Help(commands.Cog): @@ -17,11 +20,15 @@ def __init__(self, bot: Zbot): self.help_color = 8311585 self.help_color_DM = 14090153 with open('fcts/help.json', 'r', encoding="utf-8") as file: - self.commands_list = json.load(file) + self.commands_data: dict[str, CommandsCategoryData] = json.load(file) @property def doc_url(self): - return "https://zbot.readthedocs.io/en/develop/" if self.bot.beta else "https://zbot.readthedocs.io/en/latest/" + return ( + "https://zbot.readthedocs.io/en/latest/", + "https://zbot.readthedocs.io/en/develop/", + "https://zbot.readthedocs.io/en/release-candidate/", + )[self.bot.entity_id] async def cog_unload(self): self.bot.remove_command("help") @@ -33,8 +40,8 @@ async def bvn_help(self, ctx: MyContext): """Help on setting up welcome / leave messages ..Doc infos.html#welcome-message""" - prefix = await self.bot.prefix_manager.get_prefix(ctx.guild) - await ctx.send(await self.bot._(ctx.guild, "welcome.help", p=prefix)) + config_cmd = await self.bot.get_command_mention("config change") + await ctx.send(await self.bot._(ctx.guild, "welcome.help", config_cmd=config_cmd)) @commands.command(name="about", aliases=["botinfos", "botinfo"]) @commands.cooldown(7, 30, commands.BucketType.user) @@ -43,7 +50,8 @@ async def infos(self, ctx: MyContext): ..Doc infos.html#about""" urls = "" - for e, url in enumerate(['http://discord.gg/N55zY88', 'https://zrunner.me/invitezbot', 'https://zbot.rtfd.io/', 'https://twitter.com/z_runnerr', 'https://zrunner.me/zbot-faq', 'https://zrunner.me/zbot-privacy.pdf']): + bot_invite = "https://zrunner.me/" + ("invitezbot" if self.bot.entity_id == 0 else "invite-axobot") + for e, url in enumerate(['http://discord.gg/N55zY88', bot_invite, 'https://zbot.rtfd.io/', 'https://twitter.com/z_runnerr', 'https://zrunner.me/zbot-faq', 'https://zrunner.me/zbot-privacy.pdf']): urls += "\n:arrow_forward: " + await self.bot._(ctx.channel, f"info.about-{e}") + " <" + url + ">" msg = await self.bot._(ctx.channel, "info.about-main", mention=ctx.bot.user.mention, links=urls) if ctx.can_send_embed: @@ -80,7 +88,7 @@ async def help_cmd(self, ctx: MyContext, *args: str): else: await self._default_help_command(ctx, args) - async def help_command(self, ctx: MyContext, commands=()): + async def help_command(self, ctx: MyContext, commands: Optional[list[str]] = None): """Main command for the creation of the help message If the bot can't send the new command format, it will try to send the old one.""" async with ctx.channel.typing(): @@ -96,25 +104,31 @@ async def help_command(self, ctx: MyContext, commands=()): await ctx.message.author.create_dm() destination = ctx.message.author.dm_channel - me = destination.me if isinstance(destination, discord.DMChannel) else destination.guild.me + bot_usr = destination.me if isinstance(destination, discord.DMChannel) else destination.guild.me title = "" - if " ".join(commands).lower() in self.commands_list.keys(): + if commands is not None and " ".join(commands).lower() in self.commands_data: categ_name = [" ".join(commands).lower()] else: - translated_categories = {k: await self.bot._(ctx.channel, f"help.categories.{k}") for k in self.commands_list.keys()} - categ_name = [k for k, v in translated_categories.items() if v.lower() == " ".join(commands).lower()] + translated_categories = { + k: await self.bot._(ctx.channel, f"help.categories.{k}") + for k in self.commands_data.keys() + } + if commands is None: + categ_name = [] + else: + categ_name = [k for k, v in translated_categories.items() if v.lower() == " ".join(commands).lower()] if len(categ_name) == 1: # cog name if categ_name[0] == "unclassed": - referenced_commands = {x for v in self.commands_list.values() for x in v} + referenced_commands = {x for v in self.commands_data.values() for x in v['commands']} temp = [c for c in self.bot.commands if c.name not in referenced_commands] else: - temp = [c for c in self.bot.commands if c.name in self.commands_list[categ_name[0]]] + temp = [c for c in self.bot.commands if c.name in self.commands_data[categ_name[0]]['commands']] pages = await self.all_commands(ctx, sorted(temp, key=self.sort_by_name)) if len(pages) == 0 and ctx.guild is None: pages = [await self.bot._(ctx.channel, "help.cog-empty-dm")] - elif len(commands) == 0: # no command + elif not commands: # no command compress = await self.bot.get_config(ctx.guild, 'compress_help') pages = await self.all_commands(ctx, sorted([c for c in self.bot.commands], key=self.sort_by_name), compress=compress) if ctx.guild is None: @@ -135,7 +149,7 @@ async def help_command(self, ctx: MyContext, commands=()): name = await discord.ext.commands.clean_content().convert(ctx2, name) await destination.send(await self.bot._(ctx.channel, "help.cmd-not-found", cmd=name)) return - pages = await self.cmd_help(ctx, command, destination.permissions_for(me).embed_links) + pages = await self.cmd_help(ctx, command, destination.permissions_for(bot_usr).embed_links) else: # sub-command name? name = commands[0] command = self.bot.all_commands.get(name) @@ -151,7 +165,7 @@ async def help_command(self, ctx: MyContext, commands=()): except AttributeError: await destination.send(await self.bot._(ctx.channel, "help.no-subcmd", cmd=command.name)) return - pages = await self.cmd_help(ctx, command, destination.permissions_for(me).embed_links) + pages = await self.cmd_help(ctx, command, destination.permissions_for(bot_usr).embed_links) ft = await self.bot._(ctx.channel, "help.footer") prefix = await self.bot.prefix_manager.get_prefix(ctx.guild) @@ -159,7 +173,7 @@ async def help_command(self, ctx: MyContext, commands=()): self.bot.dispatch("error", ValueError(f"Unable to find help for the command {' '.join(commands)}")) await destination.send(await self.bot._(ctx.channel, "help.cmd-not-found", cmd=" ".join(commands))) return - if destination.permissions_for(me).embed_links: + if destination.permissions_for(bot_usr).embed_links: if ctx.guild is not None: embed_colour = ctx.guild.me.color if ctx.guild.me.color != discord.Colour.default() else discord.Colour(self.help_color) else: @@ -194,7 +208,7 @@ def sort_by_name(self, cmd: commands.Command) -> str: async def all_commands(self, ctx: MyContext, cmds: List[commands.Command], compress: bool = False): """Create pages for every bot command""" - categories = {x: [] for x in self.commands_list.keys()} + categories = {x: [] for x in self.commands_data.keys()} for cmd in cmds: try: if cmd.hidden or not cmd.enabled: @@ -205,8 +219,8 @@ async def all_commands(self, ctx: MyContext, cmds: List[commands.Command], compr continue temp = await self.display_cmd(cmd) found = False - for k, values in self.commands_list.items(): - if cmd.name in values: + for k, values in self.commands_data.items(): + if cmd.name in values['commands']: categories[k].append(temp) found = True break @@ -218,10 +232,11 @@ async def all_commands(self, ctx: MyContext, cmds: List[commands.Command], compr for k, values in categories.items(): if len(values) == 0: continue - tr = await self.bot._(ctx.channel, f"help.categories.{k}") - title = "__**"+tr.capitalize()+"**__" + tr_name = await self.bot._(ctx.channel, f"help.categories.{k}") + emoji = self.commands_data[k]['emoji'] + title = f"{emoji} __**{tr_name.capitalize()}**__" count = await self.bot._(ctx.channel, "help.cmd-count", - nbr=len(values), + count=len(values), p=prefix, cog=k) answer.append((title, count)) @@ -229,23 +244,24 @@ async def all_commands(self, ctx: MyContext, cmds: List[commands.Command], compr for k, values in categories.items(): if len(values) == 0: continue - tr = await self.bot._(ctx.channel, f"help.categories.{k}") + emoji = self.commands_data[k]['emoji'] + tr_name = await self.bot._(ctx.channel, f"help.categories.{k}") if len("\n".join(values)) > 1020: temp = list(values) values = [] i = 1 for line in temp: if len("\n".join(values+[line])) > 1020: - title = (tr+' - ' + str(i)) if 'help.' not in tr else (k+' - '+str(i)) - answer.append(("__**"+title.capitalize()+"**__", "\n".join(values))) + title = (tr_name+' - ' + str(i)) if 'help.' not in tr_name else (k+' - '+str(i)) + answer.append((f"{emoji} __**{title.capitalize()}**__", "\n".join(values))) values.clear() i += 1 values.append(line) - title = (tr+' - ' + str(i)) if 'help.' not in tr else (k+' - '+str(i)) - answer.append(("__**"+title.capitalize()+"**__", "\n".join(values))) + title = (tr_name+' - ' + str(i)) if 'help.' not in tr_name else (k+' - '+str(i)) + answer.append((f"{emoji} __**{title.capitalize()}**__", "\n".join(values))) else: - title = tr - answer.append(("__**"+title.capitalize()+"**__", "\n".join(values))) + title = tr_name + answer.append((f"{emoji} __**{title.capitalize()}**__", "\n".join(values))) return answer async def cog_commands(self, ctx: MyContext, cog: commands.Cog): @@ -348,26 +364,31 @@ async def cmd_help(self, ctx: MyContext, cmd: commands.core.Command, use_embed: pass_check = False if pass_check: checks.append( - ":small_blue_diamond: "+check_msg_tr[0]) + "✅ "+check_msg_tr[0]) else: checks.append('❌ '+check_msg_tr[1]) else: - self.bot.log.warning(f"No description for help check {check_name} ({c})") + self.bot.dispatch("error", ValueError(f"No description for help check {check_name} ({c})")) except Exception as err: self.bot.dispatch("error", err, f"While checking {c} in help") # Module category = "unclassed" - for k, v in self.commands_list.items(): - if cmd.name in v or cmd.full_parent_name in v: - category = k + for key, data in self.commands_data.items(): + categ_commands = data['commands'] + if cmd.name in categ_commands or (cmd.full_parent_name and cmd.full_parent_name.split(" ")[0] in categ_commands): + category = key break - category = (await self.bot._(ctx.channel, f"help.categories.{category}")).capitalize() + emoji = self.commands_data[category]['emoji'] + category = emoji + " " + (await self.bot._(ctx.channel, f"help.categories.{category}")).capitalize() if use_embed: answer = [] answer.append([f"**{prefix}{syntax}"]) answer.append((await self.bot._(ctx.channel, 'help.description'), desc)) if example is not None: - answer.append(((await self.bot._(ctx.channel, 'misc.example')).capitalize(), "\n".join(example))) + answer.append(( + (await self.bot._(ctx.channel, 'misc.example', count=len(example))).capitalize(), + "\n".join(example) + )) if len(subcmds) > 0: answer.append((await self.bot._(ctx.channel, 'help.subcmds'), subcmds)) if len(cmd.aliases) > 0: @@ -386,7 +407,9 @@ async def cmd_help(self, ctx: MyContext, cmd: commands.core.Command, use_embed: else: answer = f"**{prefix}{syntax}\n\n{desc}\n\n" if example is not None: - answer += "\n__"+(await self.bot._(ctx.channel, 'misc.example')).capitalize()+"__\n"+"\n".join(example)+"\n" + title = (await self.bot._(ctx.channel, 'misc.example', count=len(example))).capitalize() + f_examples = '\n'.join(example) + answer += f"\n__{title}__\n{f_examples}\n" if len(subcmds) > 0: answer += "\n"+subcmds+"\n" if len(cmd.aliases) > 0: @@ -403,17 +426,17 @@ async def cmd_help(self, ctx: MyContext, cmd: commands.core.Command, use_embed: return [answer] async def _default_help_command(self, ctx: MyContext, command: str = None): - truc = commands.DefaultHelpCommand() - truc.context = ctx - truc._command_impl = self.help_cmd + default_help = commands.DefaultHelpCommand() + default_help.context = ctx + default_help._command_impl = self.help_cmd # General help if command is None: - mapping = truc.get_bot_mapping() - return await truc.send_bot_help(mapping) + mapping = default_help.get_bot_mapping() + return await default_help.send_bot_help(mapping) # Check if it's a cog cog = self.bot.get_cog(" ".join(command)) if cog is not None: - return await truc.send_cog_help(cog) + return await default_help.send_cog_help(cog) # If it's not a cog then it's a command. # Since we want to have detailed errors when someone # passes an invalid subcommand, we need to walk through @@ -422,25 +445,25 @@ async def _default_help_command(self, ctx: MyContext, command: str = None): keys = command cmd = self.bot.all_commands.get(keys[0]) if cmd is None: - string = await maybe_coro(truc.command_not_found, truc.remove_mentions(keys[0])) - return await truc.send_error_message(string) + string = await maybe_coro(default_help.command_not_found, default_help.remove_mentions(keys[0])) + return await default_help.send_error_message(string) for key in keys[1:]: try: found = cmd.all_commands.get(key) except AttributeError: - string = await maybe_coro(truc.subcommand_not_found, cmd, truc.remove_mentions(key)) - return await truc.send_error_message(string) + string = await maybe_coro(default_help.subcommand_not_found, cmd, default_help.remove_mentions(key)) + return await default_help.send_error_message(string) else: if found is None: - string = await maybe_coro(truc.subcommand_not_found, cmd, truc.remove_mentions(key)) - return await truc.send_error_message(string) + string = await maybe_coro(default_help.subcommand_not_found, cmd, default_help.remove_mentions(key)) + return await default_help.send_error_message(string) cmd = found if isinstance(cmd, commands.Group): - return await truc.send_group_help(cmd) + return await default_help.send_group_help(cmd) else: - return await truc.send_command_help(cmd) + return await default_help.send_command_help(cmd) async def setup(bot): diff --git a/fcts/antiscam.py b/fcts/antiscam.py index da7c3170..e1c392ed 100644 --- a/fcts/antiscam.py +++ b/fcts/antiscam.py @@ -3,6 +3,7 @@ import typing import discord +from discord import app_commands from discord.ext import commands from libs.antiscam import AntiScamAgent, Message, update_unicode_map @@ -34,6 +35,11 @@ def __init__(self, bot: Zbot): self.file = "antiscam" self.agent = AntiScamAgent() self.table = 'messages_beta' + self.report_ctx_menu = app_commands.ContextMenu( + name='Report a scam', + callback=self.report_context_menu, + ) + self.bot.tree.add_command(self.report_ctx_menu) async def cog_load(self): "Load websites list from database" @@ -52,6 +58,10 @@ async def cog_load(self): self.agent.fetch_websites_locally() self.bot.log.info(f"[antiscam] Loaded {len(self.agent.websites_list)} domain names from local file") + async def cog_unload(self): + "Disable the report context menu" + self.bot.tree.remove_command(self.report_ctx_menu.name, type=self.report_ctx_menu.type) + @property def report_channel(self) -> discord.TextChannel: return self.bot.get_channel(913821367500148776) @@ -147,14 +157,14 @@ async def create_embed(self, msg: Message, author: discord.User, row_id: int, st pred_title = self.agent.categories[predicted.result].title() pred_value = round( predicted.probabilities[predicted.result]*100, 2) - emb.add_field(name="According to Zbot", + emb.add_field(name=f"According to {self.bot.user.name}", value=f'{pred_title} ({pred_value}%)') return emb - async def send_report(self, ctx: commands.Context, row_id: int, msg: Message): + async def send_report(self, message_author: discord.User, row_id: int, msg: Message): "Send a message report into the internal reports channel" prediction = self.agent.predict_bot(msg) - emb = await self.create_embed(msg, ctx.author, row_id, "pending", prediction) + emb = await self.create_embed(msg, message_author, row_id, "pending", prediction) await self.report_channel.send(embed=emb, view=MsgReportView(row_id)) async def edit_report_message(self, message: discord.InteractionMessage, new_status: str): @@ -187,38 +197,43 @@ async def train_model(self): ) return await train_model(data) - @commands.group(name="antiscam") + @commands.hybrid_group(name="antiscam") + @app_commands.default_permissions(manage_guild=True) async def antiscam(self, ctx: MyContext): """Everything related to the antiscam feature ..Doc moderator.html#anti-scam""" if ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx, ['antiscam']) + await ctx.send_help(ctx.command) @antiscam.command(name="test") + @app_commands.describe(text="The message to check") @commands.cooldown(5, 30, commands.BucketType.user) - async def antiscam_test(self, ctx: MyContext, *, msg: str): + async def antiscam_test(self, ctx: MyContext, *, text: str): """Test the antiscam feature with a given message ..Example antiscam test free nitro for everyone at bit.ly/tomato""" - data = Message.from_raw(msg, 0, self.agent.websites_list) + data = Message.from_raw(text, 0, self.agent.websites_list) pred = self.agent.predict_bot(data) url_score = await self.bot._(ctx.channel, "antiscam.url-score", score=data.url_score) - result_ = await self.bot._(ctx.channel, "antiscam.result") probabilities_ = await self.bot._(ctx.channel, "antiscam.probabilities") probas = '\n - '.join(f'{self.agent.categories[c]}: {round(p*100, 1)}%' for c, p in pred.probabilities.items()) - msg = f"""{result_} **{self.agent.categories[pred.result]}** - -{probabilities_} + answer = f"""{probabilities_} - {probas} + {url_score}""" - await ctx.send(msg) + embed = discord.Embed( + title = await self.bot._(ctx.channel, "antiscam.result") + " " + self.agent.categories[pred.result], + description = answer, + color=discord.Color.red() if pred.result >= 2 else discord.Color.green() + ) + await ctx.send(embed=embed) @antiscam.command(name="enable") @commands.guild_only() @commands.check(checks.has_manage_guild) async def antiscam_enable(self, ctx: MyContext): - """Enable the anti scam feature in your server + """Enable the antiscam feature in your server ..Doc moderator.html#anti-scam""" msg: discord.Message = copy.copy(ctx.message) @@ -230,7 +245,7 @@ async def antiscam_enable(self, ctx: MyContext): @commands.guild_only() @commands.check(checks.has_manage_guild) async def antiscam_disable(self, ctx: MyContext): - """Disable the anti scam feature in your server + """Disable the antiscam feature in your server ..Doc moderator.html#anti-scam""" msg: discord.Message = copy.copy(ctx.message) @@ -238,7 +253,7 @@ async def antiscam_disable(self, ctx: MyContext): new_ctx = await self.bot.get_context(msg) await self.bot.invoke(new_ctx) - @antiscam.command(name="fetch-unicode") + @antiscam.command(name="fetch-unicode", with_app_command=False) @commands.check(checks.is_bot_admin) async def antiscam_refetch_uneicode(self, ctx: MyContext): "Refetch the unicode map of confusable characters" @@ -248,7 +263,7 @@ async def antiscam_refetch_uneicode(self, ctx: MyContext): await update_unicode_map() await ctx.send("Done!") - @antiscam.command(name="update-table") + @antiscam.command(name="update-table", with_app_command=False) @commands.check(checks.is_bot_admin) async def antiscam_update_table(self, ctx: MyContext): "Update the recorded messages table" @@ -256,7 +271,7 @@ async def antiscam_update_table(self, ctx: MyContext): counter = await self.db_update_messages(self.table) await ctx.send(f"{counter} messages updated!") - @antiscam.command(name="train") + @antiscam.command(name="train", with_app_command=False) @commands.check(checks.is_bot_admin) async def antiscam_train_model(self, ctx: MyContext): "Re-train the antiscam model (DESTRUCTIVE ACTION)" @@ -276,28 +291,59 @@ async def antiscam_train_model(self, ctx: MyContext): else: txt += f"\n❌ This model is not better than the current one ({current_acc:.3f})" await msg.edit(content=txt) + + async def report_context_menu(self, interaction: discord.Interaction, message: discord.Message): + "Report a suspicious message to the bot team" + if not message.content: + await interaction.response.send_message( + await self.bot._(interaction.user, "antiscam.report-empty"), + ephemeral=True + ) + return + await interaction.response.defer(ephemeral=True) + await self._report_message(message.author, message.content, len(message.mentions), message.guild.id) + await interaction.followup.send( + await self.bot._(interaction.user, "antiscam.report-successful"), + ephemeral=True + ) @antiscam.command(name="report") @commands.cooldown(5, 30, commands.BucketType.guild) @commands.cooldown(2, 10, commands.BucketType.user) - async def antiscam_report(self, ctx: MyContext, *, message: typing.Union[discord.Message, str]): + async def antiscam_report(self, ctx: MyContext, *, message: str): """Report a suspicious message to the bot team This will help improving the bot detection AI ..Doc moderator.html#anti-scam""" - content = message.content if isinstance(message, discord.Message) else message - mentions_count = len(message.mentions) if isinstance(message, discord.Message) else 0 - msg = Message.from_raw(content, mentions_count, self.agent.websites_list) - if isinstance(message, discord.Message) and message.guild: - msg.contains_everyone = f'<@&{message.guild.id}>' in content or '@everyone' in content + await ctx.defer() + try: + src_msg = await commands.converter.MessageConverter().convert(ctx, message) + except commands.CommandError: + src_msg = None + content = message + mentions_count = 0 + author = ctx.author else: - msg.contains_everyone = '@everyone' in content - msg_id = await self.db_insert_msg(msg) - await self.send_report(ctx, msg_id, msg) + if not src_msg.content: + await ctx.send(await self.bot._(ctx, "antiscam.report-empty"), ephemeral=True) + return + content = src_msg.content + mentions_count = len(src_msg.mentions) + author = src_msg.author + await self._report_message(author, content, mentions_count, ctx.guild.id) await ctx.reply( await self.bot._(ctx.channel, "antiscam.report-successful"), allowed_mentions=discord.AllowedMentions.none() ) + + async def _report_message(self, message_author: discord.User, content: str, mentions_count: int, guild_id: typing.Optional[int]): + msg = Message.from_raw(content, mentions_count, self.agent.websites_list) + if guild_id: + msg.contains_everyone = f'<@&{guild_id}>' in content or '@everyone' in content + else: + msg.contains_everyone = '@everyone' in content + msg_id = await self.db_insert_msg(msg) + await self.send_report(message_author, msg_id, msg) @commands.Cog.listener() async def on_message(self, msg: discord.Message): @@ -329,12 +375,12 @@ async def on_message(self, msg: discord.Message): await self.send_bot_log(msg, deleted=True) self.bot.dispatch("antiscam_delete", msg, result) msg_id = await self.db_insert_msg(message) - await self.send_report(msg, msg_id, message) + await self.send_report(msg.author, msg_id, message) elif result.probabilities[1] < 0.3: await self.send_bot_log(msg, deleted=False) self.bot.dispatch("antiscam_warn", msg, result) msg_id = await self.db_insert_msg(message) - await self.send_report(msg, msg_id, message) + await self.send_report(msg.author, msg_id, message) @commands.Cog.listener() async def on_interaction(self, interaction: discord.Interaction): diff --git a/fcts/args.py b/fcts/args.py index cb39920a..608719f6 100644 --- a/fcts/args.py +++ b/fcts/args.py @@ -7,11 +7,9 @@ from libs.bot_classes import MyContext -class tempdelta(commands.Converter): - def __init__(self): - pass - - async def convert(self, ctx: MyContext, argument: str) -> int: +class tempdelta(float): + @classmethod + async def convert(cls, ctx: MyContext, argument: str) -> int: duration = 0 found = False # ctx.invoked_with @@ -41,11 +39,9 @@ async def convert(self, ctx: MyContext, argument: str) -> int: return duration -class user(commands.converter.UserConverter): - def __init__(self): - pass - - async def convert(self, ctx: MyContext, argument: str) -> discord.User: +class user(discord.User): + @classmethod + async def convert(cls, ctx: MyContext, argument: str) -> discord.User: res = None if argument.isnumeric(): if ctx.guild is not None: @@ -68,22 +64,18 @@ async def convert(self, ctx: MyContext, argument: str) -> discord.User: raise commands.errors.UserNotFound(argument) -class cardStyle(commands.Converter): - def __init__(self): - pass - - async def convert(self, ctx: MyContext, argument: str) -> str: +class cardStyle(str): + @classmethod + async def convert(cls, ctx: MyContext, argument: str) -> str: if argument in await ctx.bot.get_cog('Utilities').allowed_card_styles(ctx.author): return argument else: raise commands.errors.BadArgument('Invalid card style: '+argument) -class LeaderboardType(commands.Converter): - def __init__(self): - pass - - async def convert(self, ctx: MyContext, argument: str) -> str: +class LeaderboardTypeConverter(str): + @classmethod + async def convert(cls, ctx: MyContext, argument: str) -> str: if argument in {'server', 'guild', 'serveur', 'local'}: if ctx.guild is None: raise commands.errors.BadArgument(f'Cannot use {argument} leaderboard type outside a server') @@ -91,6 +83,7 @@ async def convert(self, ctx: MyContext, argument: str) -> str: elif argument in {'all', 'global', 'tout'}: return 'global' raise commands.errors.BadArgument(f'Invalid leaderboard type: {argument}') +LeaderboardType = typing.Annotated[typing.Literal["guild", "global"], LeaderboardTypeConverter] class Invite(commands.Converter): @@ -117,11 +110,9 @@ async def convert(self, _ctx: MyContext, argument: str) -> typing.Union[str, int return answer -class Guild(commands.Converter): - def __init__(self): - pass - - async def convert(self, ctx: MyContext, argument: str) -> discord.Guild: +class Guild(discord.Guild): + @classmethod + async def convert(cls, ctx: MyContext, argument: str) -> discord.Guild: if argument.isnumeric(): res = ctx.bot.get_guild(int(argument)) if res is not None: @@ -217,9 +208,10 @@ async def convert(cls, ctx: MyContext, argument: str) -> int: return cls(int(argument)) -class serverlog(commands.Converter): +class serverlog(str): "Convert arguments to a server log type" - async def convert(self, ctx: MyContext, argument: str) -> str: + @classmethod + async def convert(cls, ctx: MyContext, argument: str) -> str: from fcts.serverlogs import ServerLogs # pylint: disable=import-outside-toplevel if argument in ServerLogs.available_logs() or argument == 'all': diff --git a/fcts/blurple.py b/fcts/blurple.py index 1ed79b6f..afff42c9 100644 --- a/fcts/blurple.py +++ b/fcts/blurple.py @@ -91,13 +91,13 @@ async def blurple_main(self, ctx: MyContext): ..Example b blurplefy ++more-dark-blurple ++more-dark-blurple ++more-white ++less-blurple -..Example b darkfy @Zbot +..Example b darkfy @Axobot -..Example blurple check light Zbot +..Example blurple check light Axobot ..Example b check dark""" if ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx, ['blurple']) + await ctx.send_help(ctx.command) @blurple_main.command() async def help(self, ctx: MyContext): diff --git a/fcts/bot_events.py b/fcts/bot_events.py index 3ea2c473..2b3d4d5e 100644 --- a/fcts/bot_events.py +++ b/fcts/bot_events.py @@ -18,7 +18,8 @@ "april-2021": "Aujourd'hui, c'est la journée internationale des poissons ! Pendant toute la journée, Zbot fêtera le 1er avril avec des émojis spéciaux pour le jeu du morpion, un avatar unique ainsi que d'autres choses trop cool. \n\nProfitez-en pour récupérer des points d'événements et tentez de gagner la carte d'xp rainbow ! Pour rappel, les cartes d'xp sont accessibles via ma commande `profile card`", "april-2022": "Aujourd'hui, c'est la journée internationale des poissons ! Pendant toute la journée, Zbot fêtera le 1er avril avec des émojis spéciaux pour le jeu du morpion, des commandes uniques ainsi que d'autres choses trop cool. \n\nProfitez-en pour récupérer des points d'événements et tentez de gagner la carte d'xp rainbow ! Pour rappel, les cartes d'xp sont accessibles via ma commande `profile card`", "halloween-2022": "Le mois d'octobre est là ! Profitez jusqu'au 1er novembre d'une atmosphère ténébreuse, remplie de chauve-souris, de squelettes et de citrouilles.\nProfitez-en pour redécorer votre serveur aux couleurs d'Halloween avec la commande `halloween lightfy` et ses dérivées, vérifiez que votre avatar soit bien conforme avec la commande `halloween check`, et récupérez des points d'événements toutes les heures avec la commande `halloween collect`.\n\nLes plus courageux d'entre vous réussirons peut-être à débloquer la carte d'xp spécial Halloween 2022, que vous pourrez utiliser via la commande profile card !", - "christmas-2022": "La période des fêtes de fin d'année est là ! C'est l'occasion rêvée de retrouver ses amis et sa famille, de partager de bons moments ensemble, et de s'offrir tout plein de somptueux cadeaux !\n\nPour cet événement de rassemblement, nulle compétition, il vous suffit d'utiliser la commande `event collect` pour récupérer votre carte d'XP spécial Noël 2022 !\nVous pourrez ensuite utiliser cette carte d'XP via la commande `profile card`.\n\nBonne fêtes de fin d'année à tous !" + "christmas-2022": "La période des fêtes de fin d'année est là ! C'est l'occasion rêvée de retrouver ses amis et sa famille, de partager de bons moments ensemble, et de s'offrir tout plein de somptueux cadeaux !\n\nPour cet événement de rassemblement, nulle compétition, il vous suffit d'utiliser la commande `event collect` pour récupérer votre carte d'XP spécial Noël 2022 !\nVous pourrez ensuite utiliser cette carte d'XP via la commande `profile card`.\n\nBonne fêtes de fin d'année à tous !", + "test-2022": "Test event!" }, "events-prices": { "april-2021": { @@ -36,7 +37,8 @@ "april-2021": "Joyeux 1er avril !", "april-2022": "Joyeux 1er avril !", "halloween-2022": "Le temps des citrouilles est arrivé !", - "christmas-2022": "Joyeuses fêtes de fin d'année !" + "christmas-2022": "Joyeuses fêtes de fin d'année !", + "test-2022": "Test event!" } }, "en": { @@ -44,7 +46,8 @@ "april-2021": "Today is International Fish Day! All day long, Zbot will be celebrating April 1st with special tic-tac-toe emojis, a unique avatar and other cool stuff. \nTake the opportunity to collect event points and try to win the rainbow xp card! As a reminder, the xp cards are accessible via my `profile card` command", "april-2022": "Today is International Fish Day! Throughout the day, Zbot will be celebrating April 1 with special tic-tac-toe emojis, unique commands and other cool stuff. \n\nTake the opportunity to collect event points and try to win the rainbow xp card! As a reminder, the xp cards are accessible via my `profile card` command", "halloween-2022": "October is here! Enjoy a dark atmosphere full of bats, skeletons and pumpkins until November 1st.\nTake the opportunity to redecorate your server in Halloween colors with the `halloween lightfy` command and its derivatives, check your avatar with the `halloween check` command, and collect event points every hour with the `halloween collect` command.\n\nThe most courageous among you may succeed in unlocking the special Halloween 2022 xp card, which you can use via the profile card command!", - "christmas-2022": "The holiday season is here! It's the perfect opportunity to get together with friends and family, share good times together, and get all sorts of wonderful gifts!\n\nFor this gathering event, no competition, just use the `event collect` command to get your Christmas 2022 XP card!\nYou can then use this XP card via the `profile card` command.\n\nMerry Christmas to all!" + "christmas-2022": "The holiday season is here! It's the perfect opportunity to get together with friends and family, share good times together, and get all sorts of wonderful gifts!\n\nFor this gathering event, no competition, just use the `event collect` command to get your Christmas 2022 XP card!\nYou can then use this XP card via the `profile card` command.\n\nMerry Christmas to all!", + "test-2022": "Test event!" }, "events-prices": { "april-2021": { @@ -62,7 +65,8 @@ "april-2021": "Happy April 1st!", "april-2022": "Happy April 1st!", "halloween-2022": "It's pumpkin time!", - "christmas-2022": "Merry Christmas!" + "christmas-2022": "Merry Christmas!", + "test-2022": "Test event!" } } } @@ -148,7 +152,7 @@ async def on_message(self, msg: discord.Message): async def events_main(self, ctx: MyContext): """When I'm organizing some events""" if ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx, ['events']) + await ctx.send_help(ctx.command) @events_main.command(name="info") async def event_info(self, ctx: MyContext): @@ -280,6 +284,9 @@ async def events_rank(self, ctx: MyContext, user: discord.User = None): @commands.cooldown(3, 60, commands.BucketType.user) async def event_collect(self, ctx: MyContext): "Collect your Christmas present!" + if self.current_event_id != "christmas-2022": + await ctx.send(await self.bot._(ctx.channel, "bot_events.nothing-desc")) + return if (users_cog := self.bot.get_cog("Users")) is None: raise RuntimeError("Users cog not found") if await users_cog.has_rankcard(ctx.author, "christmas22"): diff --git a/fcts/bot_stats.py b/fcts/bot_stats.py index 2035e2a8..096be453 100644 --- a/fcts/bot_stats.py +++ b/fcts/bot_stats.py @@ -1,5 +1,6 @@ from datetime import datetime import math +import re import typing import aiohttp @@ -20,6 +21,10 @@ json_loads = orjson.loads +async def get_ram_data(): + data = psutil.virtual_memory() + return data.percent, (data.total - data.available) + class BotStats(commands.Cog): """Hey, I'm a test cog! Happy to meet you :wave:""" @@ -27,16 +32,20 @@ def __init__(self, bot: Zbot): self.bot = bot self.file = 'bot_stats' self.received_events = {'CMD_USE': 0} - self.commands_uses = {} + self.commands_uses: dict[str, int] = {} + self.app_commands_uses: dict[str, int] = {} self.rss_stats = {'checked': 0, 'messages': 0, 'errors': 0, 'warnings': 0} self.xp_cards = {'generated': 0, 'sent': 0} self.process = psutil.Process() - self.cpu_records: list[float] = [] + self.bot_cpu_records: list[float] = [] + self.total_cpu_records: list[float] = [] self.latency_records: list[int] = [] self.statuspage_header = {"Content-Type": "application/json", "Authorization": "OAuth " + self.bot.others["statuspage"]} self.antiscam = {"warning": 0, "deletion": 0} self.ticket_events = {"creation": 0} self.usernames = {"guild": 0, "user": 0, "deleted": 0} + self.emitted_serverlogs: dict[str, int] = {} + self.last_backup_size: typing.Optional[int] = None async def cog_load(self): # pylint: disable=no-member @@ -55,10 +64,14 @@ async def cog_unload(self): @tasks.loop(seconds=10) async def record_cpu_usage(self): "Record the CPU usage for later use" - self.cpu_records.append(self.process.cpu_percent()) - if len(self.cpu_records) > 6: + self.bot_cpu_records.append(self.process.cpu_percent()) + if len(self.bot_cpu_records) > 6: + # if the list becomes too long (over 1min), cut it + self.bot_cpu_records = self.bot_cpu_records[-6:] + self.total_cpu_records.append(psutil.cpu_percent()) + if len(self.total_cpu_records) > 6: # if the list becomes too long (over 1min), cut it - self.cpu_records = self.cpu_records[-6:] + self.total_cpu_records = self.total_cpu_records[-6:] @record_cpu_usage.error async def on_record_cpu_error(self, error: Exception): @@ -133,10 +146,28 @@ async def on_socket_raw_receive(self, msg: str): async def on_command_completion(self, ctx: MyContext): """Called when a command is correctly used by someone""" name = ctx.command.full_parent_name.split()[0] if ctx.command.parent is not None else ctx.command.name - nbr = self.commands_uses.get(name, 0) - self.commands_uses[name] = nbr + 1 - nbr = self.received_events.get('CMD_USE', 0) - self.received_events['CMD_USE'] = nbr + 1 + self.commands_uses[name] = self.commands_uses.get(name, 0) + 1 + self.received_events['CMD_USE'] = self.received_events.get('CMD_USE', 0) + 1 + if ctx.interaction: + self.app_commands_uses[name] = self.app_commands_uses.get(name, 0) + 1 + self.received_events['SLASH_CMD_USE'] = self.received_events.get('SLASH_CMD_USE', 0) + 1 + + @commands.Cog.listener() + async def on_serverlog(self, guild_id: int, channel_id: int, log_type: str): + "Called when a serverlog is emitted" + self.emitted_serverlogs[log_type] = self.emitted_serverlogs.get(log_type, 0) + 1 + + + @commands.Cog.listener() + async def on_message(self, message: discord.Message): + "Collect the last backup size from the logs channel" + if message.channel.id != 625319946271850537 or len(message.embeds) != 1: + return + embed = message.embeds[0] + if match := re.search(r"Database backup done! \((\d+(?:\.\d+)?)([GM])\)", embed.description): + unit = match.group(2) + self.last_backup_size = float(match.group(1)) / (1024 if unit == "M" else 1) + self.bot.log.info(f"Last backup size detected: {self.last_backup_size}G") async def db_get_disabled_rss(self) -> int: "Count the number of disabled RSS feeds in any guild" @@ -223,6 +254,16 @@ async def db_record_serverlogs_enabled(self, now: datetime): async with self.bot.db_query(query, (now, *args)) as _: pass + async def db_get_antiscam_enabled_count(self): + "Get the number of active guilds where antiscam is enabled" + query = f"SELECT `ID` FROM `servers` WHERE `anti_scam` = 1" + count = 0 + async with self.bot.db_query(query) as query_results: + for row in query_results: + if row["ID"] in self.bot.guilds: + count += 1 + return count + @tasks.loop(minutes=1) async def sql_loop(self): """Send our stats every minute""" @@ -247,30 +288,43 @@ async def sql_loop(self): for k, v in self.commands_uses.items(): cursor.execute(query, (now, 'cmd.'+k, v, 0, 'cmd/min', True, self.bot.entity_id)) self.commands_uses.clear() + for k, v in self.app_commands_uses.items(): + cursor.execute(query, (now, 'app_cmd.'+k, v, 0, 'cmd/min', True, self.bot.entity_id)) + self.app_commands_uses.clear() # RSS stats for k, v in self.rss_stats.items(): cursor.execute(query, (now, 'rss.'+k, v, 0, k, k == "messages", self.bot.entity_id)) cursor.execute(query, (now, 'rss.disabled', await self.db_get_disabled_rss(), 0, 'disabled', False, self.bot.entity_id)) # XP cards - cursor.execute(query, (now, 'xp.generated_cards', self.xp_cards["generated"], 0, 'cards/min', True, self.bot.entity_id)) - cursor.execute(query, (now, 'xp.sent_cards', self.xp_cards["sent"], 0, 'cards/min', True, self.bot.entity_id)) - self.xp_cards["generated"] = 0 - self.xp_cards["sent"] = 0 - # Latency - RAM usage - CPU usage - ram = round(self.process.memory_info()[0]/2.**30, 3) + if self.xp_cards["generated"]: + cursor.execute(query, (now, 'xp.generated_cards', self.xp_cards["generated"], 0, 'cards/min', True, self.bot.entity_id)) + self.xp_cards["generated"] = 0 + if self.xp_cards["sent"]: + cursor.execute(query, (now, 'xp.sent_cards', self.xp_cards["sent"], 0, 'cards/min', True, self.bot.entity_id)) + self.xp_cards["sent"] = 0 + # Latency if latency := await self.get_list_usage(self.latency_records): cursor.execute(query, (now, 'perf.latency', latency, 1, 'ms', False, self.bot.entity_id)) - cursor.execute(query, (now, 'perf.ram', ram, 1, 'Gb', False, self.bot.entity_id)) - cpu = await self.get_list_usage(self.cpu_records) - if cpu is not None: - cursor.execute(query, (now, 'perf.cpu', cpu, 1, '%', False, self.bot.entity_id)) + # CPU usage + bot_cpu = await self.get_list_usage(self.bot_cpu_records) + if bot_cpu is not None: + cursor.execute(query, (now, 'perf.bot_cpu', bot_cpu, 1, '%', False, self.bot.entity_id)) + total_cpu = await self.get_list_usage(self.total_cpu_records) + if total_cpu is not None: + cursor.execute(query, (now, 'perf.total_cpu', total_cpu, 1, '%', False, self.bot.entity_id)) + # RAM usage + bot_ram = round(self.process.memory_info()[0] / 2.**30, 3) + cursor.execute(query, (now, 'perf.bot_ram', bot_ram, 1, 'Gb', False, self.bot.entity_id)) + percent_ram, total_ram = await get_ram_data() + cursor.execute(query, (now, 'perf.total_ram', round(total_ram / 1e9, 3), 1, 'Gb', False, self.bot.entity_id)) + cursor.execute(query, (now, 'perf.percent_total_ram', percent_ram, 1, '%', False, self.bot.entity_id)) # Unavailable guilds unav, total = 0, 0 for guild in self.bot.guilds: unav += guild.unavailable total += 1 cursor.execute(query, (now, 'guilds.unavailable', round(unav/total, 3)*100, 1, '%', False, self.bot.entity_id)) - cursor.execute(query, (now, 'guilds.total', total, 0, 'guilds', True, self.bot.entity_id)) + cursor.execute(query, (now, 'guilds.total', total, 0, 'guilds', False, self.bot.entity_id)) del unav, total # antiscam warn/deletions if self.antiscam["warning"]: @@ -278,6 +332,8 @@ async def sql_loop(self): if self.antiscam["deletion"]: cursor.execute(query, (now, 'antiscam.deletion', self.antiscam["deletion"], 0, 'deletion/min', True, self.bot.entity_id)) self.antiscam["warning"] = self.antiscam["deletion"] = 0 + # antiscam activated count + cursor.execute(query, (now, 'antiscam.activated', await self.db_get_antiscam_enabled_count(), 0, 'guilds', False, self.bot.entity_id)) # tickets creation if self.ticket_events["creation"]: cursor.execute(query, (now, 'tickets.creation', self.ticket_events["creation"], 0, 'tickets/min', True, self.bot.entity_id)) @@ -296,9 +352,16 @@ async def sql_loop(self): await self.db_record_eventpoints_values(now) # serverlogs await self.db_record_serverlogs_enabled(now) + for k, v in self.emitted_serverlogs.items(): + cursor.execute(query, (now, f'logs.{k}.emitted', v, 0, 'event/min', True, self.bot.entity_id)) + self.emitted_serverlogs.clear() + # Last backup save + if self.last_backup_size: + cursor.execute(query, (now, 'backup.size', self.last_backup_size, 1, 'Gb', False, self.bot.entity_id)) + self.last_backup_size = None # Push everything cnx.commit() - except mysql.connector.errors.IntegrityError as err: # duplicate primary key + except mysql.connector.errors.IntegrityError as err: # usually duplicate primary key self.bot.log.warning(f"Stats loop iteration cancelled: {err}") # if something goes wrong, we still have to close the cursor cursor.close() @@ -310,7 +373,7 @@ async def before_sql_loop(self): @sql_loop.error async def on_sql_loop_error(self, error: Exception): - self.bot.dispatch("error", error, "When sending SQL stats") + self.bot.dispatch("error", error, "SQL stats loop has stopped <@279568324260528128>") async def get_stats(self, variable: str, minutes: int) -> typing.Union[int, float, str, None]: """Get the sum of a certain variable in the last X minutes""" @@ -320,7 +383,7 @@ async def get_stats(self, variable: str, minutes: int) -> typing.Union[int, floa result: list[dict] = list(cursor) cursor.close() if len(result) == 0: - return None + return 0 result = result[0] if result['type'] == 0: return int(result['value']) @@ -359,7 +422,7 @@ async def before_status_loop(self): @status_loop.error async def on_status_loop_error(self, error: Exception): - self.bot.dispatch("error", error, "When sending stats to statuspage.io") + self.bot.dispatch("error", error, "When sending stats to statuspage.io (<@279568324260528128>)") async def setup(bot): diff --git a/fcts/cases.py b/fcts/cases.py index b010574f..ff7c41a1 100644 --- a/fcts/cases.py +++ b/fcts/cases.py @@ -1,13 +1,17 @@ import importlib -import typing +from datetime import datetime +from math import ceil +from typing import Optional import discord from discord.ext import commands + +from fcts.checks import is_support_staff from libs.bot_classes import MyContext, Zbot from libs.formatutils import FormatUtils +from libs.paginator import Paginator from . import args -from fcts.checks import is_support_staff importlib.reload(args) @@ -20,14 +24,14 @@ async def can_edit_case(ctx: MyContext): return False class Case: - def __init__(self,bot:Zbot,guildID:int,memberID:int,Type,ModID:int,Reason,date,duration=None,caseID=None): + def __init__(self, bot: Zbot, guild_id: int, member_id: int, case_type: str, mod_id: int, reason: str, date: datetime, duration: Optional[int]=None, case_id: Optional[int]=None): self.bot = bot - self.guild = guildID - self.id = caseID - self.user = memberID - self.type = Type - self.mod = ModID - self.reason = Reason + self.guild = guild_id + self.id = case_id + self.user = member_id + self.type = case_type + self.mod = mod_id + self.reason = reason self.duration = duration if date is None: self.date = "Unknown" @@ -78,7 +82,7 @@ def __init__(self, bot: Zbot): async def on_ready(self): self.table = 'cases_beta' if self.bot.beta else 'cases' - async def get_case(self, columns=None, criters=None, relation="AND") -> typing.Optional[list[Case]]: + async def get_case(self, columns=None, criters=None, relation="AND") -> Optional[list[Case]]: """return every cases""" if not self.bot.database_online: return None @@ -98,9 +102,17 @@ async def get_case(self, columns=None, criters=None, relation="AND") -> typing.O async with self.bot.db_query(query) as query_results: if len(columns) == 0: for elem in query_results: - case = Case(bot=self.bot, guildID=elem['guild'], caseID=elem['ID'], memberID=elem['user'], - Type=elem['type'], ModID=elem['mod'], date=elem['created_at'], Reason=elem['reason'], - duration=elem['duration']) + case = Case( + bot=self.bot, + guild_id=elem['guild'], + case_id=elem['ID'], + member_id=elem['user'], + case_type=elem['type'], + mod_id=elem['mod'], + date=elem['created_at'], + reason=elem['reason'], + duration=elem['duration'] + ) liste.append(case) else: for elem in query_results: @@ -116,12 +128,12 @@ async def get_nber(self, user_id:int, guild_id:int): return query_results['count'] return 0 except Exception as err: - await self.bot.get_cog('Errors').on_error(err,None) + self.bot.dispatch("error", err) async def delete_case(self, case_id: int): """delete a case from the db""" if not self.bot.database_online: - return None + return False if not isinstance(case_id, int): raise ValueError query = ("DELETE FROM `{}` WHERE `ID`='{}'".format(self.table, case_id)) @@ -132,7 +144,7 @@ async def delete_case(self, case_id: int): async def add_case(self, case): """add a new case to the db""" if not self.bot.database_online: - return None + return False if not isinstance(case, Case): raise ValueError query = "INSERT INTO `{}` (`guild`, `user`, `type`, `mod`, `reason`,`duration`) VALUES (%(g)s, %(u)s, %(t)s, %(m)s, %(r)s, %(d)s)".format(self.table) @@ -162,7 +174,7 @@ async def case_main(self, ctx: MyContext): ..Doc moderator.html#handling-cases""" if ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx, ['cases']) + await ctx.send_help(ctx.command) @case_main.command(name="list") @commands.guild_only() @@ -176,12 +188,12 @@ async def see_case(self, ctx: MyContext, *, user:args.user): ..Doc moderator.html#view-list""" if not self.bot.database_online: return await ctx.send(await self.bot._(ctx.guild.id,'cases.no_database')) - await self.see_case_main(ctx,ctx.guild.id,user.id) + await self.see_case_main(ctx, ctx.guild.id, user) @case_main.command(name="glist") @commands.guild_only() @commands.check(is_support_staff) - async def see_case_2(self, ctx: MyContext, guild: typing.Optional[args.Guild], *, user:args.user): + async def see_case_2(self, ctx: MyContext, guild: Optional[args.Guild], *, user:args.user): """Get every case of a user on a specific guild or on every guilds This user can have left the server @@ -190,72 +202,81 @@ async def see_case_2(self, ctx: MyContext, guild: typing.Optional[args.Guild], * ..Example cases glist someone""" if not self.bot.database_online: return await ctx.send(await self.bot._(ctx.guild.id,'cases.no_database')) - await self.see_case_main(ctx, guild.id if guild else None, user.id) + await self.see_case_main(ctx, guild.id if guild else None, user) - async def see_case_main(self, ctx: MyContext, guild:discord.Guild, user:discord.User): + async def see_case_main(self, ctx: MyContext, guild: discord.Guild, user: discord.User): if guild is not None: - criters = ["`user`='{}'".format(user),"guild='{}'".format(guild)] + criters = ["`user`='{}'".format(user.id),"guild='{}'".format(guild)] syntax: str = await self.bot._(ctx.guild,'cases.list-0') else: syntax: str = await self.bot._(ctx.guild,'cases.list-1') - criters = ["`user`='{}'".format(user)] + criters = ["`user`='{}'".format(user.id)] try: MAX_CASES = 60 cases = await self.get_case(criters=criters) - total_nbr = len(cases) - cases = cases[-MAX_CASES:] cases.reverse() - u = self.bot.get_user(user) - if len(cases) == 0: + if cases is None or len(cases) == 0: await ctx.send(await self.bot._(ctx.guild.id, "cases.no-case")) return + cases: list[Case] if ctx.can_send_embed: - last_case = e = total_nbr if len(cases) > 0 else 0 - embed = discord.Embed(title="title", colour=self.bot.get_cog('Servers').embed_color, timestamp=ctx.message.created_at) - if u is None: - embed.set_author(name=str(user)) - else: - embed.set_author(name="Cases from "+str(u), icon_url=str(u.display_avatar.with_format("png"))) - embed.set_footer(text="Requested by {}".format(ctx.author), icon_url=str(ctx.author.display_avatar.with_format("png"))) - if len(cases) > 0: - l = await self.bot._(ctx.guild.id,'_used_locale') - for case in cases: - e -= 1 - guild: discord.Guild = self.bot.get_guild(case.guild) - if guild is None: - guild = case.guild - else: - guild = guild.name - mod: discord.Member = self.bot.get_user(case.mod) - if mod is None: - mod = case.mod - else: - mod = mod.mention - date_ = f"" - text = syntax.format(G=guild, T=case.type, M=mod, R=case.reason, D=date_) - if case.duration is not None and case.duration > 0: - text += "\n" + await self.bot._(ctx.guild.id,'cases.display.duration', data=await FormatUtils.time_delta(case.duration,lang=l,year=False,form="short")) - embed.add_field(name=await self.bot._(ctx.guild.id, "cases.title-search", ID=case.id), value=text, inline=False) - if len(embed.fields) == 20: - embed.title = await self.bot._(ctx.guild.id,"cases.cases-0", nbr=total_nbr, start=e+1, end=last_case) - await ctx.send(embed=embed) - embed.clear_fields() - last_case = e - if len(embed.fields) > 0: - embed.title = await self.bot._(ctx.guild.id,"cases.cases-0", nbr=total_nbr, start=e+1, end=last_case) - await ctx.send(embed=embed) + author_text = await self.bot._(ctx.guild.id, "cases.display.title", user=str(user), user_id=user.id) + title = await self.bot._(ctx.guild.id,"cases.records_number", nbr=len(cases)) + lang = await self.bot._(ctx.guild.id,'_used_locale') + + class RecordsPaginator(Paginator): + "Paginator used to display a user record" + users: dict[int, Optional[discord.User]] + + async def get_page_count(self, interaction) -> int: + length = len(cases) + if length == 0: + return 1 + return ceil(length/21) + + async def get_page_content(self, interaction, page): + "Create one page" + embed = discord.Embed(title=title, colour=self.client.get_cog('Servers').embed_color, timestamp=ctx.message.created_at) + embed.set_author(name=author_text, icon_url=str(user.display_avatar.with_format("png"))) + page_start, page_end = (page-1)*21, page*21 + for case in cases[page_start:page_end]: + guild = self.client.get_guild(case.guild) + if guild is None: + guild = case.guild + else: + guild = guild.name + mod = self.client.get_user(case.mod) + if mod is None: + mod = case.mod + else: + mod = mod.mention + date_ = f"" + text = syntax.format(G=guild, T=case.type, M=mod, R=case.reason, D=date_) + if case.duration is not None and case.duration > 0: + formated_duration = await FormatUtils.time_delta(case.duration,lang=lang,year=False,form="short") + text += "\n" + await self.client._(interaction,'cases.display.duration', data=formated_duration) + embed.add_field(name=await self.client._(interaction, "cases.title-search", ID=case.id), value=text, inline=True) + footer = f"{ctx.author} | {page}/{await self.get_page_count(interaction)}" + embed.set_footer(text=footer, icon_url=ctx.author.display_avatar) + return { + "embed": embed + } + + _quit = await self.bot._(ctx.guild, "misc.quit") + view = RecordsPaginator(self.bot, ctx.author, stop_label=_quit.capitalize()) + await view.send_init(ctx) else: if len(cases) > 0: - text = await self.bot._(ctx.guild.id,"cases.cases-0", nbr=total_nbr, start=1, end=total_nbr) + "\n" - for e, case in enumerate(cases): + text = await self.bot._(ctx.guild.id,"cases.records_number", nbr=len(cases)) + "\n" + for case in cases: text += "```{}\n```".format((await case.display(True)).replace('*','')) if len(text) > 1800: await ctx.send(text) text = "" if len(text) > 0: await ctx.send(text) - except Exception as e: - await self.bot.get_cog("Errors").on_error(e,None) + except Exception as err: + self.bot.dispatch("command_error", ctx, err) @case_main.command(name="reason",aliases=['edit']) @@ -273,8 +294,8 @@ async def reason(self, ctx: MyContext, case:int, *, reason): if not await self.bot.get_cog('Admin').check_if_admin(ctx.author): c.append("guild="+str(ctx.guild.id)) cases = await self.get_case(criters=c) - except Exception as e: - await self.bot.get_cog("Errors").on_error(e,None) + except Exception as err: + self.bot.dispatch("command_error", ctx, err) return if len(cases)!=1: await ctx.send(await self.bot._(ctx.guild.id,"cases.not-found")) @@ -304,7 +325,7 @@ async def search_case(self, ctx: MyContext, case:int): c.append("guild="+str(ctx.guild.id)) cases = await self.get_case(criters=c) except Exception as err: - await self.bot.get_cog("Errors").on_error(err,ctx) + self.bot.dispatch("command_error", ctx, err) return if len(cases)!=1: await ctx.send(await self.bot._(ctx.guild.id,"cases.not-found")) @@ -338,7 +359,7 @@ async def search_case(self, ctx: MyContext, case:int): emb.set_author(name=user, icon_url=user.display_avatar) await ctx.send(embed=emb) except Exception as err: - await self.bot.get_cog("Errors").on_error(err,ctx) + self.bot.dispatch("command_error", ctx, err) @case_main.command(name="remove", aliases=["clear", "delete"]) @@ -358,7 +379,7 @@ async def remove(self, ctx: MyContext, case:int): c.append("guild="+str(ctx.guild.id)) cases = await self.get_case(columns=['ID','user'],criters=c) except Exception as err: - await self.bot.get_cog("Errors").on_error(err,None) + self.bot.dispatch("command_error", ctx, err) return if len(cases) != 1: await ctx.send(await self.bot._(ctx.guild.id,"cases.not-found")) diff --git a/fcts/errors.py b/fcts/errors.py index fb57bfe1..339d478b 100644 --- a/fcts/errors.py +++ b/fcts/errors.py @@ -5,7 +5,7 @@ import typing import discord -from discord.ext import commands +from discord.ext import commands, tasks from libs.bot_classes import MyContext, Zbot from libs.errors import NotDuringEventError, VerboseCommandError @@ -19,7 +19,37 @@ class Errors(commands.Cog): def __init__(self, bot: Zbot): self.bot = bot self.file = "errors" + # map of user ID and number of cooldowns recently hit + self.cooldown_pool: dict[int, int] = {} + async def cog_load(self): + self.reduce_cooldown_pool.start() + + async def cog_unload(self): + if self.reduce_cooldown_pool.is_running(): + self.reduce_cooldown_pool.cancel() + + async def can_send_cooldown_error(self, user_id: int): + "Check if we can send a cooldown error message for a given user, to avoid spam" + spam_score = self.cooldown_pool.get(user_id, 0) + self.cooldown_pool[user_id] = spam_score + 1 + return spam_score < 4 + + @tasks.loop(seconds=5) + async def reduce_cooldown_pool(self): + "Reduce the cooldown score by 1 every 5s" + to_delete: set[int] = set() + for user_id in self.cooldown_pool: + self.cooldown_pool[user_id] -= 1 + if self.cooldown_pool[user_id] <= 0: + to_delete.add(user_id) + for user_id in to_delete: + del self.cooldown_pool[user_id] + + @reduce_cooldown_pool.error + async def on_reduce_cooldown_pool_error(self, error): + "Log errors from reduce_cooldown_pool" + self.bot.dispatch("error", error) @commands.Cog.listener() async def on_command_error(self, ctx: MyContext, error: Exception): @@ -63,14 +93,16 @@ async def on_command_error(self, ctx: MyContext, error: Exception): await ctx.send(await self.bot._(ctx.channel, 'errors.quoteserror'), ephemeral=True) return elif isinstance(error, NotDuringEventError): - await ctx.send(await self.bot._(ctx.channel, 'errors.notduringevent', cmd="`/event info`"), ephemeral=True) + cmd = await self.bot.get_command_mention("event info") + await ctx.send(await self.bot._(ctx.channel, 'errors.notduringevent', cmd=cmd), ephemeral=True) return elif isinstance(error,commands.errors.CommandOnCooldown): - if await self.bot.get_cog('Admin').check_if_admin(ctx): - await ctx.reinvoke() - return - d = round(error.retry_after, 2 if error.retry_after < 60 else 0) - await ctx.send(await self.bot._(ctx.channel, 'errors.cooldown', d=d), ephemeral=True) + # if await checks.is_bot_admin(ctx): + # await ctx.reinvoke() + # return + if await self.can_send_cooldown_error(ctx.author.id): + d = round(error.retry_after, 2 if error.retry_after < 60 else None) + await ctx.send(await self.bot._(ctx.channel, 'errors.cooldown', d=d), ephemeral=True) return elif isinstance(error, commands.BadLiteralArgument): await ctx.send(await self.bot._(ctx.channel, 'errors.badlitteral'), ephemeral=True) @@ -189,8 +221,9 @@ async def send_err(tr_key: str, **kwargs): await ctx.send(await self.bot._(ctx.channel,'errors.cannotembed')) return else: + cmd = await self.bot.get_command_mention("about") try: - await ctx.send(await self.bot._(ctx.channel,'errors.unknown'), ephemeral=True) + await ctx.send(await self.bot._(ctx.channel, 'errors.unknown', about=cmd), ephemeral=True) except Exception as newerror: self.bot.log.info(f"[on_cmd_error] Can't send error on channel {ctx.channel.id}: {newerror}") # All other Errors not returned come here... And we can just print the default TraceBack. diff --git a/fcts/events.py b/fcts/events.py index d2e88d8a..440cc430 100644 --- a/fcts/events.py +++ b/fcts/events.py @@ -4,17 +4,18 @@ import marshal import random import re -import shutil import time -from typing import Union +from typing import Optional import aiohttp import discord import mysql from discord.ext import commands, tasks -from libs.bot_classes import Zbot + +from libs.bot_classes import MyContext, Zbot from libs.enums import UsernameChangeRecord + class Events(commands.Cog): """Cog for the management of major events that do not belong elsewhere. Like when a new server invites the bot.""" @@ -25,7 +26,6 @@ def __init__(self, bot: Zbot): self.partner_last_check = datetime.datetime.utcfromtimestamp(0) self.last_eventDay_check = datetime.datetime.utcfromtimestamp(0) self.statslogs_last_push = datetime.datetime.utcfromtimestamp(0) - self.last_statusio = datetime.datetime.utcfromtimestamp(0) self.loop_errors = [0,datetime.datetime.utcfromtimestamp(0)] self.last_membercounter = datetime.datetime.utcfromtimestamp(0) self.embed_colors = {"welcome":5301186, @@ -70,49 +70,64 @@ async def on_ready(self): @commands.Cog.listener() async def on_member_update(self, before: discord.Member, after: discord.Member): """Called when a member change something (status, activity, nickame, roles)""" - if before.nick != after.nick: - await self.updade_memberslogs_name(before, after) + if before.nick != after.nick and await self.can_register_memberlog(before.id, before.guild.id): + await self.register_memberlog_name(before, after) @commands.Cog.listener() async def on_user_update(self, before: discord.User, after: discord.User): """Called when a user change something (avatar, username, discrim)""" - if before.name != after.name: - await self.updade_memberslogs_name(before, after) - - async def updade_memberslogs_name(self, before: Union[discord.User, discord.Member], after: Union[discord.User, discord.Member], tries:int=0): - "Save in our database when a user change its nickname or username" - config_option = await self.bot.get_cog('Utilities').get_db_userinfo(['allow_usernames_logs'],["userID="+str(before.id)]) - if config_option is not None and config_option['allow_usernames_logs'] is False: + if before.name != after.name and await self.can_register_memberlog(before.id): + await self.register_userlog_name(before, after) + + async def can_register_memberlog(self, user_id: int, guild_id: Optional[int]=None): + "Check if we are allowed to register a memberlog (if database is online, if the user has allowed it, if the server hasn't disabled it)" + if not self.bot.database_online: + return False + # If axobot is already there, let it handle it + if guild_id and await self.bot.check_axobot_presence(guild_id=guild_id): return + config_option = await self.bot.get_cog('Utilities').get_db_userinfo(['allow_usernames_logs'],["userID=" + str(user_id)]) + if config_option is not None and config_option['allow_usernames_logs'] is False: + return False + if guild_id: + return await self.bot.get_config(guild_id, "nicknames_history") + return True + + async def register_memberlog_name(self, before: discord.Member, after: discord.Member, tries:int=0): + "Save in our database when a user change its nickname" if tries > 5: self.bot.dispatch("error", RuntimeError(f"Nickname change failed after 5 attempts for user {before.id}")) return - if not self.bot.database_online: + before_nick = '' if before.nick is None else before.nick + after_nick = '' if after.nick is None else after.nick + try: + await self.db_register_memberlog(before.id, before_nick, after_nick, before.guild.id) + except mysql.connector.errors.IntegrityError as err: + self.bot.log.warning(err) + await self.register_memberlog_name(before, after, tries+1) + + async def register_userlog_name(self, before: discord.User, after: discord.User, tries:int=0): + "Save in our database when a user change its username" + if tries > 5: + self.bot.dispatch("error", RuntimeError(f"Nickname change failed after 5 attempts for user {before.id}")) return - if isinstance(before, discord.Member): - # If axobot is already there, let it handle it - if await self.bot.check_axobot_presence(guild=before.guild): - return - if not await self.bot.get_config(before.guild.id, "nicknames_history"): - return - before_nick = '' if before.nick is None else before.nick - after_nick = '' if after.nick is None else after.nick - else: - before_nick = '' if before.name is None else before.name - after_nick = '' if after.name is None else after.name - guild = before.guild.id if hasattr(before, 'guild') else 0 - query = "INSERT INTO `usernames_logs` (`user`,`old`,`new`,`guild`,`beta`) VALUES (%(u)s,%(o)s,%(n)s,%(g)s,%(b)s)" - query_args = { 'u': before.id, 'o': before_nick, 'n': after_nick, 'g': guild, 'b': self.bot.beta } + before_nick = '' if before.name is None else before.name + after_nick = '' if after.name is None else after.name try: - async with self.bot.db_query(query, query_args): - self.bot.dispatch("username_change_record", UsernameChangeRecord( - before_nick or None, - after_nick or None, - after - )) + await self.db_register_memberlog(before.id, before_nick, after_nick) except mysql.connector.errors.IntegrityError as err: self.bot.log.warning(err) - await self.updade_memberslogs_name(before, after, tries+1) + await self.register_userlog_name(before, after, tries+1) + + async def db_register_memberlog(self, user_id: int, before: str, after: str, guild_id: int=0): + query = "INSERT INTO `usernames_logs` (`user`,`old`,`new`,`guild`,`beta`) VALUES (%(u)s, %(o)s, %(n)s, %(g)s, %(b)s)" + query_args = { 'u': user_id, 'o': before, 'n': after, 'g': guild_id, 'b': self.bot.beta } + async with self.bot.db_query(query, query_args): + self.bot.dispatch("username_change_record", UsernameChangeRecord( + before or None, + after or None, + after + )) @commands.Cog.listener() @@ -135,7 +150,7 @@ async def send_guild_log(self, guild: discord.Guild, log_type: str): try: if log_type == "join": self.bot.log.info(f"Bot joined the server {guild.id}") - desc = f"Bot **joined the server** ({guild.name} ({guild.id}) - {len(guild.members)} users" + desc = f"Bot **joined the server** {guild.name} ({guild.id}) - {len(guild.members)} users" else: self.bot.log.info(f"Bot left the server {guild.id}") if guild.name is None and guild.unavailable: @@ -250,9 +265,9 @@ async def send_logs_per_server(self, guild: discord.Guild, log_type:str, message config = str(await self.bot.get_config(guild.id,"modlogs_channel")).split(';', maxsplit=1)[0] if config == "" or not config.isnumeric(): return - channel = guild.get_channel(int(config)) + channel = guild.get_channel_or_thread(int(config)) except Exception as err: - await self.bot.get_cog("Errors").on_error(err,None) + self.bot.dispatch("error", err) return if channel is None: return @@ -284,7 +299,7 @@ async def add_event(self, event: str): async def check_user_left(self, member: discord.Member): - """Vérifie si un joueur a été banni ou kick par ZBot""" + "Check if someone has been kicked or banned by the bot" try: async for entry in member.guild.audit_logs(user=member.guild.me, limit=15): if entry.created_at < self.bot.utcnow()-datetime.timedelta(seconds=60): @@ -331,7 +346,7 @@ async def loop(self): await self.bot.get_cog('Servers').update_everyMembercounter() self.last_membercounter = now except Exception as err: - await self.bot.get_cog('Errors').on_error(err,None) + self.bot.dispatch("error", err) self.loop_errors[0] += 1 if (datetime.datetime.now() - self.loop_errors[1]).total_seconds() > 120: self.loop_errors[0] = 0 @@ -433,7 +448,7 @@ async def partners_loop(self): count[0] += 1 count[1] += await self.bot.get_cog('Partners').update_partners(chan,guild['partner_color']) except Exception as err: - await self.bot.get_cog('Errors').on_error(err,None) + self.bot.dispatch("error", err) delta_time = round(time.time()-t,3) emb = discord.Embed( description=f'**Partners channels updated** in {delta_time}s ({count[0]} channels - {count[1]} partners)', @@ -486,5 +501,25 @@ async def send_sql_statslogs(self): self.statslogs_last_push = datetime.datetime.now() + @commands.Cog.listener() + async def on_command_completion(self, ctx: MyContext): + "If a command is executed with Zbot, remind the user to invite Axobot instead" + class MigrationView(discord.ui.View): + def __init__(self): + super().__init__() + self.add_item(discord.ui.Button(label='Invite Axobot', url="https://zrunner.me/invite-axobot", style=discord.ButtonStyle.blurple)) + self.add_item(discord.ui.Button(label='About the migration', url="https://zbot.readthedocs.io/en/release-candidate/v4.html#new-identity")) + self.add_item(discord.ui.Button(label='Support server', url="https://discord.gg/N55zY88")) + + if self.bot.entity_id == 0 and random.random() < 0.1: + txt = """Hey, Zbot is currently changing its identity to **Axobot**! + +During the migration period, Zbot will continue to work, but will **receive updates later** than Axobot and may not work as well. +Luckily for you, **the migration is very quick**, just invite Axobot and give it the same roles as Zbot to avoid any service interruption!""" + emb = discord.Embed(title="Zbot is becoming Axobot !", description=txt, color=0x00ff00) + emb.set_thumbnail(url="https://zrunner.me/axolotl.png") + await ctx.send(embed=emb, view=MigrationView()) + + async def setup(bot): await bot.add_cog(Events(bot)) diff --git a/fcts/fun.py b/fcts/fun.py index 16d69979..990dfd10 100644 --- a/fcts/fun.py +++ b/fcts/fun.py @@ -63,13 +63,14 @@ def __init__(self, bot: Zbot): def utilities(self) -> 'Utilities': return self.bot.get_cog("Utilities") - async def is_on_guild(self, user: discord.Member, guild: int): + async def is_on_guild(self, user: discord.Member, guild_id: int): "Check if a member is part of a guild" if self.bot.user.id == 436835675304755200: return True + # Zrunner, someone, Awhikax if user.id in {279568324260528128, 392766377078816789, 281404141841022976}: return True - server = self.bot.get_guild(guild) + server = self.bot.get_guild(guild_id) if server is not None: return user in server.members return False @@ -476,7 +477,11 @@ async def say(self, ctx:MyContext, channel:typing.Optional[typing.Union[discord. ..Doc miscellaneous.html#say""" if channel is None: channel = ctx.channel - elif not (channel.permissions_for(ctx.author).read_messages and channel.permissions_for(ctx.author).send_messages and channel.guild == ctx.guild): + elif not (( + channel.permissions_for(ctx.author).read_messages and + channel.permissions_for(ctx.author).send_messages and + channel.guild == ctx.guild + ) or await self.bot.get_cog('Admin').check_if_god(ctx)): await ctx.send(await self.bot._(ctx.guild, 'fun.say-no-perm', channel=channel.mention)) return if self.bot.zombie_mode: @@ -493,7 +498,7 @@ async def say(self, ctx:MyContext, channel:typing.Optional[typing.Union[discord. try: text = await self.utilities.clear_msg(text, ctx=ctx) except Exception as err: - await self.bot.get_cog('Errors').on_error(err, ctx) + self.bot.dispatch("command_error", ctx, err) return try: if not channel.permissions_for(ctx.guild.me).send_messages: @@ -543,7 +548,7 @@ async def react(self, ctx:MyContext, message:discord.Message, *, reactions): await ctx.send(await self.bot._(ctx.channel,"fun.no-emoji")) return except Exception as err: - await self.bot.get_cog("Errors").on_error(err,ctx) + self.bot.dispatch("command_error", ctx, err) continue await ctx.message.delete(delay=0) @@ -658,7 +663,17 @@ async def tip(self, ctx:MyContext): """Send a tip, a fun fact or something else ..Doc fun.html#tip""" - await ctx.send(random.choice(await self.bot._(ctx.guild,"fun.tip-list"))) + params = { + "about_cmd": await self.bot.get_command_mention("about"), + "bigtext_cmd": await self.bot.get_command_mention("bigtext"), + "clear_cmd": await self.bot.get_command_mention("clear"), + "config_cmd": await self.bot.get_command_mention("config"), + "discordlinks_cmd": await self.bot.get_command_mention("discordlinks"), + "event_cmd": await self.bot.get_command_mention("event info"), + "stats_cmd": await self.bot.get_command_mention("stats"), + "say_cmd": await self.bot.get_command_mention("say"), + } + await ctx.send(random.choice(await self.bot._(ctx.guild, "fun.tip-list", **params))) @commands.command(name='afk') @commands.check(is_fun_enabled) @@ -762,7 +777,7 @@ async def send_embed(self, ctx: MyContext, *, arguments): r = re.search(r'<#(\d+)>', arguments.split(" ")[0]) if r is not None: arguments = " ".join(arguments.split(" ")[1:]) - channel = ctx.guild.get_channel(int(r.group(1))) + channel = ctx.guild.get_channel_or_thread(int(r.group(1))) arguments = await args.arguments().convert(ctx, arguments) if len(arguments) == 0: raise commands.errors.MissingRequiredArgument(ctx.command.clean_params['arguments']) @@ -995,8 +1010,8 @@ async def vote(self, ctx: MyContext, number:typing.Optional[int] = 0, *, text): try: await self.add_vote(msg) except Exception as err: - await ctx.send(await self.bot._(ctx.channel,"fun.no-reaction")) - await self.bot.get_cog("Errors").on_error(err, ctx) + await ctx.send(await self.bot._(ctx.channel, "fun.no-reaction")) + self.bot.dispatch("error", err, ctx) return else: if ctx.bot_permissions.external_emojis: @@ -1013,7 +1028,7 @@ async def vote(self, ctx: MyContext, number:typing.Optional[int] = 0, *, text): except discord.errors.NotFound: return except Exception as err: - await self.bot.get_cog('Errors').on_error(err,ctx) + self.bot.dispatch("command_error", ctx, err) await ctx.message.delete(delay=0) async def check_suggestion(self, message: discord.Message): @@ -1030,7 +1045,7 @@ async def check_suggestion(self, message: discord.Message): except discord.DiscordException: pass except Exception as err: # pylint: disable=broad-except - await self.bot.get_cog('Errors').on_error(err,message) + self.bot.dispatch("error", err, message) @commands.command(name="pep8", aliases=['autopep8']) @commands.cooldown(3, 30, commands.BucketType.user) @@ -1046,43 +1061,6 @@ async def autopep8_cmd(self, ctx: MyContext, *, code: str): }).strip() await ctx.send(f"```py\n{code}\n```") - @commands.command(name="movie") - @commands.cooldown(5, 40, commands.BucketType.user) - @commands.check(checks.bot_can_embed) - async def movie_search(self, ctx: MyContext, *, movie_name: str): - """Search for a movie information based on its name - - Warning: Only english names are supported for now! - - ..Example movie The Circle""" - params = { - "apikey": self.bot.others["omdb"] - } - if re.match(r'tt\d+', movie_name): - params['i'] = movie_name - else: - params['t'] = movie_name - async with aiohttp.ClientSession() as session: - async with session.get("http://www.omdbapi.com/", params=params) as resp: - data = await resp.json() - if data["Response"] == "False": - await ctx.send(await self.bot._(ctx.channel,"fun.movie.not-found")) - return - website = data["Website"] if data["Website"] != "N/A" else None - rating = data["imdbRating"] if data["imdbRating"] != "N/A" else await self.bot._(ctx.channel,"fun.movie.no-rating") - description = data["Plot"] if data["Plot"] != "N/A" else await self.bot._(ctx.channel,"fun.movie.no-description") - embed = discord.Embed(title=data["Title"], url=website, description=description, color=0x3498DB) - embed.set_thumbnail(url=data["Poster"]) - embed.add_field(name=await self.bot._(ctx.channel,"fun.movie.director"), value=data["Director"]) - embed.add_field(name=await self.bot._(ctx.channel,"fun.movie.actors"), value=data["Actors"]) - embed.add_field(name=await self.bot._(ctx.channel,"fun.movie.released"), value=data["Released"]) - embed.add_field(name=await self.bot._(ctx.channel,"fun.movie.awards"), value=data["Awards"]) - embed.add_field(name=await self.bot._(ctx.channel,"fun.movie.runtime"), value=data["Runtime"]) - embed.add_field(name=await self.bot._(ctx.channel,"fun.movie.writers"), value=data["Writer"]) - embed.add_field(name=await self.bot._(ctx.channel,"fun.movie.imdb-rating"), value=rating) - embed.add_field(name=await self.bot._(ctx.channel,"fun.movie.imdb-id"), value=data["imdbID"]) - await ctx.send(embed=embed) - async def setup(bot): await bot.add_cog(Fun(bot)) diff --git a/fcts/halloween.py b/fcts/halloween.py index 6a22af72..e0249d11 100644 --- a/fcts/halloween.py +++ b/fcts/halloween.py @@ -64,15 +64,15 @@ async def hallow_main(self, ctx: MyContext): ..Example halloween lightfy ++more-dark-halloween ++more-dark-halloween ++more-white ++less-halloween -..Example halloween darkfy @Zbot +..Example halloween darkfy @Axobot -..Example halloween check light Zbot +..Example halloween check light Axobot ..Example halloween check dark ..Example halloween collect""" if ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx, ['halloween']) + await ctx.send_help(ctx.command) async def edit_img_color(self, fmodifier: typing.Literal["light", "dark"], ctx: MyContext, diff --git a/fcts/help.json b/fcts/help.json index b96b2636..d468e902 100644 --- a/fcts/help.json +++ b/fcts/help.json @@ -1,11 +1,131 @@ { - "data": ["about","admins","botinvite","changelog","discordlinks","docs","help","info","markdown","membercount","ping","prefix","rss","stats","tip"], - "moderation": ["antiscam","backup","ban","banlist","cases","clear","destop","kick","mute","modlogs","mutelist","set_xp","slowmode","softban","unban","unhoist","unmute","voice-clean","warn"], - "server-settings": ["config","mute-config","partner","pin","role","roles_react","roles_rewards","ticket","welcome"], - "users-info": ["perms","profile","rank","top","usernames"], - "other": ["book","discordjobs","discordstatus","embed","emoji","minecraft","pep8","react","remindme", "reminder"], - "fun": ["afk","arapproved","bitly","cookie","events","nasa","tic-tac-toe","fish","fun","hour","say","unafk","vote"], - "translators": ["translators"], - "staff": ["add_cog","admin","bug","del_cog","eval","execute","find","idea","msg","rss_loop","spoil","test"], - "unclassed": [] + "data": { + "emoji": "ℹ️", + "commands": [ + "about", + "admins", + "botinvite", + "changelog", + "discordlinks", + "docs", + "help", + "info", + "markdown", + "membercount", + "ping", + "prefix", + "rss", + "stats", + "tip" + ] + }, + "moderation": { + "emoji": "🔨", + "commands": [ + "antiscam", + "backup", + "ban", + "banlist", + "cases", + "clear", + "destop", + "kick", + "mute", + "modlogs", + "mutelist", + "set_xp", + "slowmode", + "softban", + "unban", + "unhoist", + "unmute", + "voice-clean", + "warn" + ] + }, + "server-settings": { + "emoji": "⚙️", + "commands": [ + "config", + "emoji", + "mute-config", + "partner", + "pin", + "role", + "roles_react", + "roles_rewards", + "tickets", + "twitch", + "welcome" + ] + }, + "users-info": { + "emoji": "👥", + "commands": [ + "permissions", + "profile", + "rank", + "top", + "usernames" + ] + }, + "other": { + "emoji": "👀", + "commands": [ + "book", + "discordjobs", + "discordstatus", + "embed", + "pep8", + "react", + "remindme", + "reminder" + ] + }, + "fun": { + "emoji": "🎳", + "commands": [ + "afk", + "arapproved", + "bitly", + "cookie", + "events", + "minecraft", + "nasa", + "tic-tac-toe", + "fish", + "fun", + "hour", + "say", + "unafk", + "vote" + ] + }, + "translators": { + "emoji": "🖋️", + "commands": [ + "translators" + ] + }, + "staff": { + "emoji": "👷", + "commands": [ + "add_cog", + "admin", + "bug", + "del_cog", + "eval", + "execute", + "find", + "idea", + "msg", + "rss_loop", + "spoil", + "test" + ] + }, + "unclassed": { + "emoji": "🤷", + "commands": [] + } } \ No newline at end of file diff --git a/fcts/info.py b/fcts/info.py index 96b6b5b3..c2c731d2 100644 --- a/fcts/info.py +++ b/fcts/info.py @@ -37,7 +37,7 @@ async def in_support_server(ctx): return ctx.guild is not None and ctx.guild.id == 625316773771608074 class Info(commands.Cog): - """Here you will find various useful commands to get information about ZBot.""" + "Here you will find various useful commands to get information about anything" def __init__(self, bot: Zbot): self.bot = bot @@ -56,7 +56,7 @@ async def on_ready(self): @commands.command(name='admins') async def admin_list(self, ctx: MyContext): - """Get the list of ZBot administrators + """Get the list of the bot administrators ..Doc miscellaneous.html#admins""" l = [] @@ -67,7 +67,7 @@ async def admin_list(self, ctx: MyContext): await ctx.send(await self.bot._(ctx.channel,"info.admins-list", admins=", ".join(l))) async def get_guilds_count(self, ignored_guilds:list=None) -> int: - """Get the number of guilds where Zbot is""" + """Get the number of guilds where the bot is""" if ignored_guilds is None: if self.bot.database_online: if 'banned_guilds' not in self.bot.get_cog('Utilities').config.keys(): @@ -100,7 +100,7 @@ async def stats_general(self, ctx: MyContext): # RAM/CPU ram_usage = round(self.process.memory_info()[0]/2.**30,3) if cog := self.bot.get_cog("BotStats"): - cpu: float = await cog.get_list_usage(cog.cpu_records) + cpu: float = await cog.get_list_usage(cog.bot_cpu_records) else: cpu = 0.0 # Guilds count @@ -126,8 +126,8 @@ async def stats_general(self, ctx: MyContext): cmds_24h = await self.bot.get_cog("BotStats").get_stats("wsevent.CMD_USE", 60*24) # number formatter lang = await self.bot._(ctx.guild.id,"_used_locale") - async def n_format(nbr): - return await FormatUtils.format_nbr(nbr, lang) + async def n_format(nbr: typing.Union[int, float, None]): + return await FormatUtils.format_nbr(nbr, lang) if nbr is not None else "0" # Generating message d = "" for key, var in [ @@ -165,7 +165,7 @@ async def stats_commands(self, ctx: MyContext): """List the most used commands ..Doc infos.html#statistics""" - forbidden = ['cmd.eval', 'cmd.admin', 'cmd.test', 'cmd.remindme'] + forbidden = ['cmd.eval', 'cmd.admin', 'cmd.test', 'cmd.remindme', 'cmd.bug', 'cmd.idea', 'cmd.send_msg'] forbidden_where = ', '.join(f"'{elem}'" for elem in forbidden) commands_limit = 15 lang = await self.bot._(ctx.channel, '_used_locale') @@ -188,7 +188,7 @@ async def do_query(minutes: typing.Optional[int] = None): `variable` LIKE "cmd.%" AND {date_where_clause} `variable` NOT IN ({forbidden_where}) AND - `beta` = %(beta)s + `entity_id` = %(entity_id)s ) UNION ALL ( SELECT `variable`, @@ -198,12 +198,12 @@ async def do_query(minutes: typing.Optional[int] = None): `variable` LIKE "cmd.%" AND {date_where_clause} `variable` NOT IN ({forbidden_where}) AND - `beta` = %(beta)s + `entity_id` = %(entity_id)s ) ) AS `all` GROUP BY cmd ORDER BY usages DESC LIMIT %(limit)s""" - async with self.bot.db_query(query, { "beta": self.bot.beta, "minutes": minutes, "limit": commands_limit }) as query_result: + async with self.bot.db_query(query, { "entity_id": self.bot.entity_id, "minutes": minutes, "limit": commands_limit }) as query_result: pass return query_result @@ -238,16 +238,16 @@ async def botinvite(self, ctx:MyContext): ..Doc infos.html#bot-invite""" raw_oauth = "<" + discord.utils.oauth_url(self.bot.user.id) + ">" + url = "https://zrunner.me/" + ("invitezbot" if self.bot.entity_id == 0 else "invite-axobot") try: - r = requests.get("https://zrunner.me/invitezbot", timeout=3) + r = requests.get(url, timeout=3) except requests.exceptions.Timeout: url = raw_oauth else: - if r.status_code < 400: - url = "https://zrunner.me/invitezbot" - else: + if r.status_code >= 400: url = raw_oauth - await ctx.send(await self.bot._(ctx.channel, "info.botinvite", url=url)) + cmd = await self.bot.get_command_mention("about") + await ctx.send(await self.bot._(ctx.channel, "info.botinvite", url=url, about=cmd)) @commands.command(name="pig", hidden=True) async def pig(self, ctx: MyContext): @@ -312,8 +312,10 @@ async def display_doc(self, ctx: MyContext): """Get the documentation url""" text = self.bot.emojis_manager.customs['readthedocs'] + await self.bot._(ctx.channel,"info.docs") + \ " https://zbot.rtfd.io" - if self.bot.beta: + if self.bot.entity_id == 1: text += '/en/develop' + elif self.bot.entity_id == 2: + text += '/en/release-candidate' await ctx.send(text) async def display_critical(self, ctx: MyContext): @@ -637,8 +639,8 @@ async def textChannel_infos(self, ctx: MyContext, channel: discord.TextChannel): # Webhooks count try: web = len(await channel.webhooks()) - except Exception as e: - await self.bot.get_cog('Errors').on_error(e,ctx) + except Exception as err: + self.bot.dispatch("error", err, ctx) web = await self.bot._(ctx.guild.id,"info.info.textchan-4") embed.add_field(name=await self.bot._(ctx.guild.id,"info.info.textchan-3"), value=str(web)) # Members nber @@ -738,11 +740,10 @@ async def guild_info(self, ctx: MyContext): # Splash url try: embed.add_field(name=await self.bot._(ctx.guild.id,"info.info.guild-15"), value=str(await guild.vanity_invite())) - except Exception as e: - if isinstance(e,(discord.errors.Forbidden, discord.errors.HTTPException)): - pass - else: - await self.bot.get_cog('Errors').on_error(e,ctx) + except (discord.errors.Forbidden, discord.errors.HTTPException): + pass + except Exception as err: + self.bot.dispatch("error", err, ctx) # Premium subscriptions count if isinstance(guild.premium_subscription_count,int) and guild.premium_subscription_count > 0: embed.add_field(name=await self.bot._(ctx.guild.id,"info.info.guild-13"), value=await self.bot._(ctx.guild.id,"info.info.guild-13v",b=guild.premium_subscription_count,p=guild.premium_tier)) @@ -752,8 +753,8 @@ async def guild_info(self, ctx: MyContext): roles = [x.mention for x in guild.roles if len(x.members) > 1][1:] else: roles = [x.name for x in guild.roles if len(x.members) > 1][1:] - except Exception as e: - await self.bot.get_cog('Errors').on_error(e,ctx) + except Exception as err: + self.bot.dispatch("error", err, ctx) roles = guild.roles roles.reverse() if len(roles) == 0: @@ -802,8 +803,8 @@ async def invite_info(self, ctx: MyContext, invite: discord.Invite): invite = temp[0] except discord.errors.Forbidden: pass - except Exception as e: - await self.bot.get_cog('Errors').on_error(e,ctx) + except Exception as err: + self.bot.dispatch("error", err, ctx) # Invite URL embed.add_field(name=await self.bot._(ctx.guild.id,"info.info.inv-0"), value=invite.url,inline=True) # Inviter @@ -981,23 +982,30 @@ async def find_guild(self, interaction: discord.Interaction, guild: str): # Bots bots = len([x for x in guild.members if x.bot]) # Lang - lang = await self.bot.get_config(guild.id,'language') + lang = await self.bot.get_config(guild.id, "language") if lang is None: lang = 'default' else: - lang = self.bot.get_cog('Languages').languages[lang] + lang = self.bot.get_cog("Languages").languages[lang] # Roles rewards - rr_len = await self.bot.get_config(guild.id,'rr_max_number') - rr_len = self.bot.get_cog("Servers").default_opt['rr_max_number'] if rr_len is None else rr_len - rr_len = '{}/{}'.format(len(await self.bot.get_cog('Xp').rr_list_role(guild.id)),rr_len) + rr_len = await self.bot.get_config(guild.id, "rr_max_number") + rr_len = '{}/{}'.format(len(await self.bot.get_cog("Xp").rr_list_role(guild.id)), rr_len) + # Streamers + if twitch_cog := self.bot.get_cog("Twitch"): + streamers_len = await self.bot.get_config(guild.id, "streamers_max_number") + streamers_len = '{}/{}'.format(await twitch_cog.db_get_guild_subscriptions_count(guild.id), streamers_len) + else: + streamers_len = "Not available" # Prefix pref = await self.bot.prefix_manager.get_prefix(guild) if "`" not in pref: pref = "`" + pref + "`" # Rss rss_len = await self.bot.get_config(guild.id,'rss_max_number') - rss_len = self.bot.get_cog("Servers").default_opt['rss_max_number'] if rss_len is None else rss_len - rss_numb = "{}/{}".format(len(await self.bot.get_cog('Rss').db_get_guild_feeds(guild.id)), rss_len) + if rss_cog := self.bot.get_cog("Rss"): + rss_numb = "{}/{}".format(len(await rss_cog.db_get_guild_feeds(guild.id)), rss_len) + else: + rss_numb = "Not available" # Join date joined_at = f"" # ---- @@ -1016,6 +1024,7 @@ async def find_guild(self, interaction: discord.Interaction, guild: str): emb.add_field(name="Prefix", value=pref) emb.add_field(name="RSS feeds count", value=rss_numb) emb.add_field(name="Roles rewards count", value=rr_len) + emb.add_field(name="Streamers count", value=streamers_len) await interaction.response.send_message(embed=emb) @find_main.command(name='channel') @@ -1195,8 +1204,8 @@ async def emoji_analysis(self, msg: discord.Message): for data in [{ 'i': x, 'l': current_timestamp } for x in liste]: async with self.bot.db_query(query, data): pass - except Exception as e: - await self.bot.get_cog('Errors').on_error(e,None) + except Exception as err: + self.bot.dispatch("error", err) async def get_emojis_info(self, ID: typing.Union[int,list]): """Get info about an emoji""" @@ -1221,7 +1230,7 @@ async def bitly_main(self, ctx: MyContext): ..Doc miscellaneous.html#bitly-urls""" if ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx,['bitly']) + await ctx.send_help(ctx.command) elif ctx.invoked_subcommand is None and ctx.subcommand_passed is not None: try: url = await args.URL.convert(ctx,ctx.subcommand_passed) diff --git a/fcts/languages.py b/fcts/languages.py index b3a7ba35..0637df3d 100644 --- a/fcts/languages.py +++ b/fcts/languages.py @@ -1,10 +1,11 @@ from typing import Union import discord import i18n -from libs.bot_classes import Zbot +from libs.bot_classes import MyContext, Zbot SourceType = Union[None, int, discord.Guild, discord.TextChannel, discord.Thread, - discord.Member, discord.User, discord.DMChannel, discord.Interaction] + discord.Member, discord.User, discord.DMChannel, discord.Interaction, + MyContext] class Languages(discord.ext.commands.Cog): @@ -44,6 +45,12 @@ async def tr(self, source: SourceType, string_id: str, **args): source = source.user else: source = None + elif isinstance(source, MyContext): + # get ID from guild + if source.guild: + source = source.guild.id + else: + source = source.author if isinstance(source, (discord.Member, discord.User)): # get lang from user diff --git a/fcts/library.py b/fcts/library.py index c595ce1c..1fca0f16 100644 --- a/fcts/library.py +++ b/fcts/library.py @@ -110,7 +110,7 @@ async def book_main(self, ctx: MyContext): ..Doc miscellaneous.html#book""" if ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx, ['book']) + await ctx.send_help(ctx.command) @book_main.command(name="search", aliases=["book"]) @commands.cooldown(5, 60, commands.BucketType.guild) diff --git a/fcts/minecraft.py b/fcts/minecraft.py index ee8e4963..5d6abdc2 100644 --- a/fcts/minecraft.py +++ b/fcts/minecraft.py @@ -101,14 +101,14 @@ async def mc_main(self, ctx: MyContext): ..Doc minecraft.html#mc""" if ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx, ['minecraft']) + await ctx.send_help(ctx.command) async def send_embed(self, ctx: MyContext, embed: discord.Embed): "Try to send an embed into a channel, or report the error if it fails" try: await ctx.send(embed=embed) except discord.DiscordException as err: - await self.bot.get_cog('Errors').on_error(err, ctx) + self.bot.dispatch("error", err, ctx) await ctx.send(await self.bot._(ctx.channel, "minecraft.serv-error")) @mc_main.command(name="block", aliases=["bloc"]) @@ -425,8 +425,9 @@ async def mc_add_server(self, ctx: MyContext, ip, port: int = None): await self.bot.get_cog('Rss').db_add_feed(ctx.guild.id, ctx.channel.id, 'mc', f"{ip}:{port}") await ctx.send(await self.bot._(ctx.guild, "minecraft.success-add", ip=display_ip, channel=ctx.channel.mention)) except Exception as err: - await ctx.send(await self.bot._(ctx.guild, "rss.fail-add")) - await self.bot.get_cog("Errors").on_error(err, ctx) + cmd = await self.bot.get_command_mention("about") + await ctx.send(await self.bot._(ctx.guild, "errors.unknown2", about=cmd)) + self.bot.dispatch("error", err, ctx) async def create_server_1(self, guild: discord.Guild, ip: str, port=None) -> Union[str, 'MCServer']: "Collect and serialize server data from a given IP, using minetools.eu" @@ -482,10 +483,10 @@ async def create_server_2(self, guild: discord.Guild, ip: str, port: str): try: r = requests.get("https://api.mcsrvstat.us/1/" + str(ip), timeout=5).json() - except Exception as e: - if not isinstance(e, requests.exceptions.ReadTimeout): + except Exception as err: + if not isinstance(err, requests.exceptions.ReadTimeout): self.bot.log.error("[mc-server-2] Erreur sur l'url {} :".format(url)) - await self.bot.get_cog('Errors').on_error(e, None) + self.bot.dispatch("error", err) return await self.bot._(guild, "minecraft.serv-error") if r["debug"]["ping"] is False: return await self.bot._(guild, "minecraft.no-ping") @@ -626,7 +627,7 @@ async def check_feed(self, feed: FeedObject, send_stats: bool): return False self.feeds[feed.link] = obj try: - channel = guild.get_channel(feed.channel_id) + channel = guild.get_channel_or_thread(feed.channel_id) if channel is None: self.bot.log.warn("[minecraft feed] Cannot find channel %s in guild %s", feed.channel_id, feed.guild_id) return False @@ -645,7 +646,7 @@ async def check_feed(self, feed: FeedObject, send_stats: bool): statscog.rss_stats['messages'] += 1 return True except Exception as err: - await self.bot.get_cog('Errors').on_error(err, None) + self.bot.dispatch("error", err) async def setup(bot): diff --git a/fcts/moderation.py b/fcts/moderation.py index a5d0068b..dfe466dd 100644 --- a/fcts/moderation.py +++ b/fcts/moderation.py @@ -1,4 +1,3 @@ -import asyncio import copy import datetime import importlib @@ -8,10 +7,12 @@ from typing import Dict, List, Literal, Optional, Tuple, Union import discord +from discord import app_commands from discord.ext import commands from libs.bot_classes import MyContext, Zbot from libs.formatutils import FormatUtils from libs.paginator import Paginator +from libs.views import ConfirmView from . import args, checks from fcts.cases import Case @@ -29,64 +30,65 @@ def __init__(self, bot: Zbot): # maximum of roles granted/revoked by query self.max_roles_modifications = 300 - @commands.command(name="slowmode") + @commands.hybrid_command(name="slowmode") + @app_commands.default_permissions(manage_channels=True) @commands.guild_only() @commands.cooldown(1, 3, commands.BucketType.guild) @commands.check(checks.can_slowmode) - async def slowmode(self, ctx: MyContext, time=None): + async def slowmode(self, ctx: MyContext, seconds: int = 0): """Keep your chat cool Slowmode works up to one message every 6h (21600s) ..Example slowmode 10 -..Example slowmode off +..Example slowmode 0 ..Doc moderator.html#slowmode""" if not ctx.channel.permissions_for(ctx.guild.me).manage_channels: await ctx.send(await self.bot._(ctx.guild.id, "moderation.no-perm")) return - if time is None: - return await ctx.send(await self.bot._(ctx.guild.id, "moderation.slowmode.info", s=ctx.channel.slowmode_delay)) - if time.isnumeric(): - time = int(time) - if time == 'off' or time == 0: + if seconds == 0: await ctx.channel.edit(slowmode_delay=0) message = await self.bot._(ctx.guild.id, "moderation.slowmode.disabled") - log = await self.bot._(ctx.guild.id,"logs.slowmode-disabled", channel=ctx.channel.mention) - await self.bot.get_cog("Events").send_logs_per_server(ctx.guild,"slowmode",log,ctx.author) - elif isinstance(time, int): - if time > 21600: - message = await self.bot._(ctx.guild.id, "moderation.slowmode.too-long") - else: - await ctx.channel.edit(slowmode_delay=time) - message = await self.bot._(ctx.guild.id, "moderation.slowmode.enabled", channel=ctx.channel.mention, s=time) - log = await self.bot._(ctx.guild.id,"logs.slowmode-enabled", channel=ctx.channel.mention, seconds=time) - await self.bot.get_cog("Events").send_logs_per_server(ctx.guild,"slowmode",log,ctx.author) + log = await self.bot._(ctx.guild.id, "logs.slowmode-disabled", channel=ctx.channel.mention) + await self.bot.get_cog("Events").send_logs_per_server(ctx.guild, "slowmode", log, ctx.author) + elif seconds > 21600: + message = await self.bot._(ctx.guild.id, "moderation.slowmode.too-long") else: - message = await self.bot._(ctx.guild.id, "moderation.slowmode.invalid") + await ctx.channel.edit(slowmode_delay=seconds) + duration = await FormatUtils.time_delta(seconds, lang=await self.bot._(ctx, "_used_locale")) + message = await self.bot._(ctx.guild.id, "moderation.slowmode.enabled", channel=ctx.channel.mention, s=duration) + log = await self.bot._(ctx.guild.id, "logs.slowmode-enabled", channel=ctx.channel.mention, seconds=duration) + await self.bot.get_cog("Events").send_logs_per_server(ctx.guild, "slowmode", log, ctx.author) await ctx.send(message) - @commands.command(name="clear") + @commands.hybrid_command(name="clear") + @app_commands.default_permissions(manage_messages=True) + @app_commands.describe(number="The max amount of messages to delete") + @app_commands.describe(users="list of user IDs or mentions to delete messages from, separated by spaces") + @app_commands.describe(contains_file="Clear only messages that contains (or does not) files") + @app_commands.describe(contains_url="Clear only messages that contains (or does not) links") + @app_commands.describe(contains_invite="Clear only messages that contains (or does not) Discord invites") + @app_commands.describe(is_pinned="Clear only messages that are (or are not) pinned") @commands.cooldown(4, 30, commands.BucketType.guild) @commands.guild_only() @commands.check(checks.can_clear) - async def clear(self, ctx: MyContext, number:int, *, params=''): + async def clear(self, ctx: MyContext, number:int, users: commands.Greedy[discord.User]=None, *, contains_file: Optional[bool]=None, contains_url: Optional[bool]=None, contains_invite: Optional[bool]=None, is_pinned: Optional[bool]=None): """Keep your chat clean - : number of messages to check - Available parameters : - <@user> : list of users to check (just mention them) - ('-f' or) '+f' : delete if the message (does not) contain any file - ('-l' or) '+l' : delete if the message (does not) contain any link - ('-p' or) '+p' : delete if the message is (not) pinned - ('-i' or) '+i' : delete if the message (does not) contain a Discord invite - By default, the bot will not delete pinned messages + : number of messages to check + [users]: list of user IDs or mentions to delete messages from + [contains_file]: delete if the message does (or does not) contain any file + [contains_url]: delete if the message does (or does not) contain any link + [contains_invite] : delete if the message does (or does not) contain a Discord invite + [is_pinned]: delete if the message is (or is not) pinned + By default, the bot will NOT delete pinned messages ..Example clear 120 ..Example clear 10 @someone -..Example clear 50 +f +l -p +..Example clear 50 False False True ..Doc moderator.html#clear""" if not ctx.channel.permissions_for(ctx.guild.me).manage_messages: @@ -95,92 +97,71 @@ async def clear(self, ctx: MyContext, number:int, *, params=''): if not ctx.channel.permissions_for(ctx.guild.me).read_message_history: await ctx.send(await self.bot._(ctx.guild.id, "moderation.need-read-history")) return - if number<1: + if number < 1: await ctx.send(await self.bot._(ctx.guild.id, "moderation.clear.too-few")+" "+self.bot.emojis_manager.customs["owo"]) return - if len(params) == 0: + await ctx.defer() + if users is None and contains_file is None and contains_url is None and contains_invite is None and is_pinned is None: return await self.clear_simple(ctx,number) - #file - if "-f" in params: - files = 0 - elif "+f" in params: - files = 2 - else: - files = 1 - #link - if "-l" in params: - links = 0 - elif "+l" in params: - links = 2 - else: - links = 1 - #pinned - if '-p' in params: - pinned = 0 - elif "+p" in params: - pinned = 2 - else: - pinned = 1 - #invite - if '-i' in params: - invites = 0 - elif "+i" in params: - invites = 2 + + def check(msg: discord.Message): + # do not delete invocation message + if ctx.interaction is not None and msg.interaction is not None and ctx.interaction.id == msg.interaction.id: + return False + if is_pinned is not None and msg.pinned != is_pinned: + return False + elif is_pinned is None and msg.pinned: + return False + if contains_file is not None and bool(msg.attachments) != contains_file: + return False + r = self.bot.get_cog("Utilities").sync_check_any_link(msg.content) + if contains_url is not None and (r is not None) != contains_url: + return False + i = self.bot.get_cog("Utilities").sync_check_discord_invite(msg.content) + if contains_invite is not None and (i is not None) != contains_invite: + return False + if users and msg.author is not None: + return str(msg.author.id) in users + return True + + if ctx.interaction: + # we'll have to check past the command answer + number += 1 else: - invites = 1 - # 0: does - 2: does not - 1: why do we care? - def check(m): - i = self.bot.get_cog("Utilities").sync_check_discord_invite(m.content) - r = self.bot.get_cog("Utilities").sync_check_any_link(m.content) - c1 = c2 = c3 = c4 = True - if pinned != 1: - if (m.pinned and pinned == 0) or (not m.pinned and pinned==2): - c1 = False - else: - c1 = not m.pinned - if files != 1: - if (m.attachments != [] and files == 0) or (m.attachments==[] and files==2): - c2 = False - if links != 1: - if (r is None and links==2) or (r is not None and links == 0): - c3 = False - if invites != 1: - if (i is None and invites==2) or (i is not None and invites == 0): - c4 = False - #return ((m.pinned == pinned) or ((m.attachments != []) == files) or ((r is not None) == links)) and m.author in users - mentions = list(map(int, re.findall(r'<@!?(\d{167,19})>', ctx.message.content))) - if str(ctx.bot.user.id) in ctx.prefix: - mentions.remove(ctx.bot.user.id) - if mentions and m.author is not None: - return c1 and c2 and c3 and c4 and m.author.id in mentions - else: - return c1 and c2 and c3 and c4 - try: + # start by deleting the invocation message await ctx.message.delete() - deleted = await ctx.channel.purge(limit=number, check=check) - await ctx.send(await self.bot._(ctx.guild, "moderation.clear.done", count=len(deleted)), delete_after=2.0) - if len(deleted) > 0: - log = await self.bot._(ctx.guild.id, "logs.clear", channel=ctx.channel.mention, number=len(deleted)) - await self.bot.get_cog("Events").send_logs_per_server(ctx.guild,"clear",log,ctx.author) - except Exception as e: - await self.bot.get_cog('Errors').on_command_error(ctx,e) + deleted = await ctx.channel.purge(limit=number, check=check) + await ctx.send(await self.bot._(ctx.guild, "moderation.clear.done", count=len(deleted)), delete_after=2.0) + if len(deleted) > 0: + log = await self.bot._(ctx.guild.id, "logs.clear", channel=ctx.channel.mention, number=len(deleted)) + await self.bot.get_cog("Events").send_logs_per_server(ctx.guild, "clear", log, ctx.author) async def clear_simple(self, ctx: MyContext, number: int): - def check(m): - return not m.pinned - try: + def check(msg: discord.Message): + if msg.pinned or msg.id == ctx.message.id: + return False + if ctx.interaction is None or msg.interaction is None: + return True + return ctx.interaction.id != msg.interaction.id + if ctx.interaction: + # we'll have to check past the command answer + number += 1 + else: + # start by deleting the invocation message await ctx.message.delete() + try: deleted = await ctx.channel.purge(limit=number, check=check) await ctx.send(await self.bot._(ctx.guild, "moderation.clear.done", count=len(deleted)), delete_after=2.0) log = await self.bot._(ctx.guild.id, "logs.clear", channel=ctx.channel.mention, number=len(deleted)) await self.bot.get_cog("Events").send_logs_per_server(ctx.guild,"clear",log,ctx.author) except discord.errors.NotFound: await ctx.send(await self.bot._(ctx.guild, "moderation.clear.not-found")) - except Exception as e: - await self.bot.get_cog('Errors').on_command_error(ctx,e) + except Exception as err: + self.bot.dispatch("command_error", ctx, err) - @commands.command(name="kick") + @commands.hybrid_command(name="kick") + @app_commands.default_permissions(kick_members=True) @commands.cooldown(5, 20, commands.BucketType.guild) @commands.guild_only() @commands.check(checks.can_kick) @@ -194,12 +175,15 @@ async def kick(self, ctx: MyContext, user:discord.Member, *, reason="Unspecified if not ctx.channel.permissions_for(ctx.guild.me).kick_members: await ctx.send(await self.bot._(ctx.guild.id, "moderation.kick.no-perm")) return + await ctx.defer() + async def user_can_kick(user): try: return await self.bot.get_cog("Servers").staff_finder(user, "kick_allowed_roles") except commands.errors.CommandError: pass return False + if user == ctx.guild.me or (self.bot.database_online and await user_can_kick(user)): return await ctx.send(await self.bot._(ctx.guild.id, "moderation.kick.cant-staff")) elif not self.bot.database_online and ctx.channel.permissions_for(user).kick_members: @@ -209,20 +193,20 @@ async def user_can_kick(user): return # send DM await self.dm_user(user, "kick", ctx, reason = None if reason=="Unspecified" else reason) - reason = await self.bot.get_cog("Utilities").clear_msg(reason,everyone = not ctx.channel.permissions_for(ctx.author).mention_everyone) + reason = await self.bot.get_cog("Utilities").clear_msg(reason, everyone = not ctx.channel.permissions_for(ctx.author).mention_everyone) await ctx.guild.kick(user,reason=reason[:512]) caseID = "'Unsaved'" if self.bot.database_online: Cases = self.bot.get_cog('Cases') - case = Case(bot=self.bot,guildID=ctx.guild.id,memberID=user.id,Type="kick",ModID=ctx.author.id,Reason=reason,date=ctx.bot.utcnow()) + case = Case(bot=self.bot,guild_id=ctx.guild.id,member_id=user.id,case_type="kick",mod_id=ctx.author.id,reason=reason,date=ctx.bot.utcnow()) try: await Cases.add_case(case) caseID = case.id - except Exception as e: - await self.bot.get_cog('Errors').on_error(e, ctx) + except Exception as err: + self.bot.dispatch("error", err, ctx) try: await ctx.message.delete() - except discord.Forbidden: + except (discord.Forbidden, discord.NotFound): pass # optional values opt_case = None if caseID=="'Unsaved'" else caseID @@ -233,13 +217,15 @@ async def user_can_kick(user): await self.send_modlogs("kick", user, ctx.author, ctx.guild, opt_case, opt_reason) except discord.errors.Forbidden: await ctx.send(await self.bot._(ctx.guild.id, "moderation.kick.too-high")) - except Exception as e: + except Exception as err: await ctx.send(await self.bot._(ctx.guild.id, "moderation.error")) - await self.bot.get_cog('Errors').on_error(e,ctx) + self.bot.dispatch("error", err, ctx) await self.bot.get_cog('Events').add_event('kick') - @commands.command(name="warn") + @commands.hybrid_command(name="warn") + @app_commands.default_permissions(moderate_members=True) + @app_commands.describe(message="The reason of the warn") @commands.cooldown(5, 20, commands.BucketType.guild) @commands.guild_only() @commands.check(checks.can_warn) @@ -273,7 +259,7 @@ async def user_can_warn(user): caseID = "'Unsaved'" if self.bot.database_online: if cases_cog := self.bot.get_cog('Cases'): - case = Case(bot=self.bot,guildID=ctx.guild.id,memberID=user.id,Type="warn",ModID=ctx.author.id,Reason=message,date=ctx.bot.utcnow()) + case = Case(bot=self.bot,guild_id=ctx.guild.id,member_id=user.id,case_type="warn",mod_id=ctx.author.id,reason=message,date=ctx.bot.utcnow()) await cases_cog.add_case(case) caseID = case.id else: @@ -286,7 +272,7 @@ async def user_can_warn(user): await self.send_modlogs("warn", user, ctx.author, ctx.guild, opt_case, message) try: await ctx.message.delete() - except discord.Forbidden: + except (discord.Forbidden, discord.NotFound): pass except Exception as err: await ctx.send(await self.bot._(ctx.guild.id, "moderation.error")) @@ -294,7 +280,7 @@ async def user_can_warn(user): async def get_muted_role(self, guild: discord.Guild): "Find the muted role from the guild config option" - opt = await self.bot.get_config(guild.id,'muted_role') + opt = await self.bot.get_config(guild.id, 'muted_role') if opt is None or (isinstance(opt, str) and not opt.isdigit()): return None # return discord.utils.find(lambda x: x.name.lower() == "muted", guild.roles) @@ -341,13 +327,15 @@ async def check_mute_context(self, ctx: MyContext, role: discord.Role, user: dis return False return True - @commands.command(name="mute") + @commands.hybrid_command(name="mute") + @app_commands.default_permissions(moderate_members=True) + @app_commands.describe(time="The duration of the mute, example 3d 7h 12min") + @app_commands.describe(reason="The reason of the mute") @commands.cooldown(5,20, commands.BucketType.guild) @commands.guild_only() @commands.check(checks.can_mute) async def mute(self, ctx: MyContext, user: discord.Member, time: commands.Greedy[args.tempdelta], *, reason="Unspecified"): - """Mute someone. -When someone is muted, the bot adds the role "muted" to them + """Timeout someone. You can also mute this member for a defined duration, then use the following format: `XXm` : XX minutes `XXh` : XX hours @@ -364,24 +352,27 @@ async def mute(self, ctx: MyContext, user: discord.Member, time: commands.Greedy if duration > 60*60*24*365*3: # max 3 years await ctx.send(await self.bot._(ctx.channel, "timers.rmd.too-long")) return - f_duration = await FormatUtils.time_delta(duration,lang=await self.bot._(ctx.guild,'_used_locale'),form="short") + f_duration = await FormatUtils.time_delta(duration, lang=await self.bot._(ctx.guild,'_used_locale'), form="short") else: f_duration = None + await ctx.defer() + + async def user_can_mute(user): + try: + return await self.bot.get_cog("Servers").staff_finder(user, "mute_allowed_roles") + except commands.errors.CommandError: + pass + return False + try: - async def user_can_mute(user): - try: - return await self.bot.get_cog("Servers").staff_finder(user, "mute_allowed_roles") - except commands.errors.CommandError: - pass - return False if user==ctx.guild.me or (self.bot.database_online and await user_can_mute(user)): emoji = random.choice([':confused:',':upside_down:',self.bot.emojis_manager.customs['wat'],':no_mouth:',self.bot.emojis_manager.customs['owo'],':thinking:',]) - await ctx.send((await self.bot._(ctx.guild.id, "moderation.mute.staff-mute"))+emoji) + await ctx.send((await self.bot._(ctx.guild.id, "moderation.mute.staff-mute")) + " " + emoji) return elif not self.bot.database_online and ctx.channel.permissions_for(user).manage_roles: return await ctx.send(await self.bot._(ctx.guild.id, "moderation.warn.cant-staff")) except Exception as err: - await self.bot.get_cog('Errors').on_error(err,ctx) + self.bot.dispatch("command_error", ctx, err) return role = await self.get_muted_role(ctx.guild) if not await self.check_mute_context(ctx, role, user): @@ -392,9 +383,9 @@ async def user_can_mute(user): if self.bot.database_online: Cases = self.bot.get_cog('Cases') if f_duration is None: - case = Case(bot=self.bot,guildID=ctx.guild.id,memberID=user.id,Type="mute",ModID=ctx.author.id,Reason=reason,date=ctx.bot.utcnow()) + case = Case(bot=self.bot,guild_id=ctx.guild.id,member_id=user.id,case_type="mute",mod_id=ctx.author.id,reason=reason,date=ctx.bot.utcnow()) else: - case = Case(bot=self.bot,guildID=ctx.guild.id,memberID=user.id,Type="tempmute",ModID=ctx.author.id,Reason=reason,date=ctx.bot.utcnow(),duration=duration) + case = Case(bot=self.bot,guild_id=ctx.guild.id,member_id=user.id,case_type="tempmute",mod_id=ctx.author.id,reason=reason,date=ctx.bot.utcnow(),duration=duration) await self.bot.task_handler.add_task('mute',duration,user.id,ctx.guild.id) try: await Cases.add_case(case) @@ -412,7 +403,7 @@ async def user_can_mute(user): await self.send_chat_answer("mute", user, ctx, opt_case) try: await ctx.message.delete() - except discord.Forbidden: + except (discord.Forbidden, discord.NotFound): pass except Exception as err: await ctx.send(await self.bot._(ctx.guild.id, "moderation.error")) @@ -453,12 +444,12 @@ async def is_muted(self, guild: discord.Guild, user: discord.User, role: Optiona result: int = query_results[0]['count'] return bool(result) - async def bdd_muted_list(self, guild_id: int, reasons: bool = False) -> Union[List[Dict[int, str]], List[int]]: + async def bdd_muted_list(self, guild_id: int, reasons: bool = False) -> Union[Dict[int, str], List[int]]: """List muted users for a specific guild Set 'reasons' to True if you want the attached reason""" if reasons: cases_table = "cases_beta" if self.bot.beta else "cases" - query = f'SELECT userid, (SELECT reason FROM {cases_table} WHERE {cases_table}.user=userid AND {cases_table}.guild=guildid AND {cases_table}.type="mute" ORDER BY `{cases_table}`.`created_at` DESC LIMIT 1) as reason FROM `mutes` WHERE guildid=%s' + query = f'SELECT userid, (SELECT reason FROM {cases_table} WHERE {cases_table}.user=userid AND {cases_table}.guild=guildid AND {cases_table}.type LIKE "%mute" ORDER BY `{cases_table}`.`created_at` DESC LIMIT 1) as reason FROM `mutes` WHERE guildid=%s' async with self.bot.db_query(query, (guild_id,)) as query_results: result = {row['userid']: row['reason'] for row in query_results} else: @@ -467,7 +458,8 @@ async def bdd_muted_list(self, guild_id: int, reasons: bool = False) -> Union[Li result = [row['userid'] for row in query_results] return result - @commands.command(name="unmute") + @commands.hybrid_command(name="unmute") + @app_commands.default_permissions(moderate_members=True) @commands.cooldown(5,20, commands.BucketType.guild) @commands.guild_only() @commands.check(checks.can_mute) @@ -497,47 +489,68 @@ async def unmute(self, ctx:MyContext, *, user:discord.Member): if user.top_role.position >= ctx.guild.me.roles[-1].position: await ctx.send(await self.bot._(ctx.guild.id, "moderation.mute.too-high")) return + await ctx.defer() try: await self.unmute_event(ctx.guild, user, ctx.author) # send in chat await self.send_chat_answer("unmute", user, ctx) try: await ctx.message.delete() - except discord.Forbidden: + except (discord.Forbidden, discord.NotFound): pass # remove planned automatic unmutes await self.bot.task_handler.cancel_unmute(user.id, ctx.guild.id) except Exception as err: await ctx.send(await self.bot._(ctx.guild.id, "moderation.error")) - await self.bot.get_cog('Errors').on_error(err,ctx) + self.bot.dispatch("command_error", ctx, err) - @commands.command(name="mute-config") - @commands.cooldown(1,15, commands.BucketType.guild) + @commands.hybrid_command(name="mute-config") + @app_commands.default_permissions(manage_roles=True) + @commands.cooldown(1, 15, commands.BucketType.guild) @commands.guild_only() - @commands.has_guild_permissions(manage_roles=True) + @commands.check(checks.has_manage_roles) async def mute_config(self, ctx: MyContext): - """Auto configure the muted role for you + """Auto configure the muted role for you, if you do not want to use the official timeout Useful if you want to have a base for a properly working muted role Warning: the process may break some things in your server, depending on how you configured your channel permissions. ..Doc moderator.html#mute-unmute """ + await ctx.defer() + confirm_view = ConfirmView( + self.bot, ctx.channel, + validation=lambda inter: inter.user == ctx.author, + ephemeral=False) + await confirm_view.init() + confirm_txt = await self.bot._(ctx.guild.id, "moderation.mute-config.confirm") + confirm_txt += "\n\n" + await self.bot._(ctx.guild.id, "moderation.mute-config.tip", mute=await self.bot.get_command_mention("mute")) + confirm_msg = await ctx.send(confirm_txt, view=confirm_view) + + await confirm_view.wait() + await confirm_view.disable(confirm_msg) + if not confirm_view.value: + return + role = await self.get_muted_role(ctx.guild) create = role is None role, count = await self.configure_muted_role(ctx.guild, role) if role is None or count >= len(ctx.guild.voice_channels+ctx.guild.text_channels): - await ctx.send(await self.bot._(ctx.guild.id, "moderation.mute.mute-config-err")) + await ctx.send(await self.bot._(ctx.guild.id, "moderation.mute-config.err")) elif create: - await ctx.send(await self.bot._(ctx.guild.id, "moderation.mute.mute-config-success", count=count)) + await ctx.send(await self.bot._(ctx.guild.id, "moderation.mute-config.success", count=count)) else: - await ctx.send(await self.bot._(ctx.guild.id, "moderation.mute.mute-config-success2", count=count)) + await ctx.send(await self.bot._(ctx.guild.id, "moderation.mute-config.success2", count=count)) - @commands.command(name="ban") + @commands.hybrid_command(name="ban") + @app_commands.default_permissions(ban_members=True) + @app_commands.describe(time="The duration of the ban, example 3d 7h 12min") + @app_commands.describe(days_to_delete="How many days of messages to delete, max 7") + @app_commands.describe(reason="The reason of the ban") @commands.cooldown(5,20, commands.BucketType.guild) @commands.guild_only() @commands.check(checks.can_ban) - async def ban(self,ctx:MyContext,user:args.user,time:commands.Greedy[args.tempdelta],days_to_delete:Optional[int]=0,*,reason="Unspecified"): + async def ban(self, ctx:MyContext, user:args.user, time:commands.Greedy[args.tempdelta]=None, days_to_delete:Optional[int]=0, *, reason="Unspecified"): """Ban someone The 'days_to_delete' option represents the number of days worth of messages to delete from the user in the guild, bewteen 0 and 7 @@ -550,7 +563,7 @@ async def ban(self,ctx:MyContext,user:args.user,time:commands.Greedy[args.tempde ..Doc moderator.html#ban-unban """ try: - duration = sum(time) + duration = sum(time) if time else 0 if duration > 0: if duration > 60*60*24*365*20: # max 20 years await ctx.send(await self.bot._(ctx.channel, "timers.rmd.too-long")) @@ -583,24 +596,24 @@ async def user_can_ban(user): if days_to_delete not in range(8): days_to_delete = 0 reason = await self.bot.get_cog("Utilities").clear_msg(reason,everyone = not ctx.channel.permissions_for(ctx.author).mention_everyone) - await ctx.guild.ban(user,reason=reason[:512],delete_message_days=days_to_delete) + await ctx.guild.ban(user, reason=reason[:512], delete_message_seconds=days_to_delete * 86400) await self.bot.get_cog('Events').add_event('ban') case_id = "'Unsaved'" if self.bot.database_online: - Cases = self.bot.get_cog('Cases') + cases_cog = self.bot.get_cog('Cases') if f_duration is None: - case = Case(bot=self.bot,guildID=ctx.guild.id,memberID=user.id,Type="ban",ModID=ctx.author.id,Reason=reason,date=ctx.bot.utcnow()) + case = Case(bot=self.bot,guild_id=ctx.guild.id,member_id=user.id,case_type="ban",mod_id=ctx.author.id,reason=reason,date=ctx.bot.utcnow()) else: - case = Case(bot=self.bot,guildID=ctx.guild.id,memberID=user.id,Type="tempban",ModID=ctx.author.id,Reason=reason,date=ctx.bot.utcnow(),duration=duration) + case = Case(bot=self.bot,guild_id=ctx.guild.id,member_id=user.id,case_type="tempban",mod_id=ctx.author.id,reason=reason,date=ctx.bot.utcnow(),duration=duration) await self.bot.task_handler.add_task('ban',duration,user.id,ctx.guild.id) try: - await Cases.add_case(case) + await cases_cog.add_case(case) case_id = case.id except Exception as err: - await self.bot.get_cog('Errors').on_error(err,ctx) + self.bot.dispatch("error", err, ctx) try: await ctx.message.delete() - except discord.Forbidden: + except (discord.Forbidden, discord.NotFound): pass # optional values opt_case = None if case_id=="'Unsaved'" else case_id @@ -613,7 +626,7 @@ async def user_can_ban(user): await ctx.send(await self.bot._(ctx.guild.id, "moderation.ban.too-high")) except Exception as err: await ctx.send(await self.bot._(ctx.guild.id, "moderation.error")) - await self.bot.get_cog('Errors').on_error(err,ctx) + self.bot.dispatch("error", err, ctx) async def unban_event(self, guild: discord.Guild, user: discord.User, author: discord.User): if not guild.me.guild_permissions.ban_members: @@ -623,7 +636,9 @@ async def unban_event(self, guild: discord.Guild, user: discord.User, author: di # send in modlogs await self.send_modlogs("unban", user, author, guild, reason="Automod") - @commands.command(name="unban") + @commands.hybrid_command(name="unban") + @app_commands.default_permissions(ban_members=True) + @app_commands.describe(reason="The reason of the unban") @commands.cooldown(5,20, commands.BucketType.guild) @commands.guild_only() @commands.check(checks.can_ban) @@ -646,11 +661,12 @@ async def unban(self, ctx: MyContext, user: str, *, reason="Unspecified"): return del backup else: - await ctx.send(ctx.guild.id, "errors.usernotfound", u=user) + await ctx.send(await self.bot._(ctx.guild.id, "errors.usernotfound", u=user)) return if not ctx.channel.permissions_for(ctx.guild.me).ban_members: await ctx.send(await self.bot._(ctx.guild.id, "moderation.ban.cant-ban")) return + await ctx.defer() try: await ctx.guild.fetch_ban(user) except discord.NotFound: @@ -661,15 +677,15 @@ async def unban(self, ctx: MyContext, user: str, *, reason="Unspecified"): case_id = "'Unsaved'" if self.bot.database_online: cases_cog = self.bot.get_cog('Cases') - case = Case(bot=self.bot,guildID=ctx.guild.id,memberID=user.id,Type="unban",ModID=ctx.author.id,Reason=reason,date=ctx.bot.utcnow()) + case = Case(bot=self.bot,guild_id=ctx.guild.id,member_id=user.id,case_type="unban",mod_id=ctx.author.id,reason=reason,date=ctx.bot.utcnow()) try: await cases_cog.add_case(case) case_id = case.id - except Exception as e: - await self.bot.get_cog('Errors').on_error(e,ctx) + except Exception as err: + self.bot.dispatch("error", err, ctx) try: await ctx.message.delete() - except discord.Forbidden: + except (discord.Forbidden, discord.NotFound): pass # optional values opt_case = None if case_id=="'Unsaved'" else case_id @@ -678,14 +694,16 @@ async def unban(self, ctx: MyContext, user: str, *, reason="Unspecified"): await self.send_chat_answer("unban", user, ctx, opt_case) # send in modlogs await self.send_modlogs("unban", user, ctx.author, ctx.guild, opt_case, opt_reason) - except Exception as e: + except Exception as err: await ctx.send(await self.bot._(ctx.guild.id, "moderation.error")) - await self.bot.get_cog('Errors').on_error(e,ctx) + self.bot.dispatch("error", err, ctx) - @commands.command(name="softban") + @commands.hybrid_command(name="softban") + @app_commands.default_permissions(kick_members=True) + @app_commands.describe(reason="The reason of the kick") @commands.guild_only() @commands.check(checks.can_kick) - async def softban(self, ctx: MyContext, user:discord.Member, *, reason="Unspecified"): + async def softban(self, ctx: MyContext, user: discord.Member, *, reason="Unspecified"): """Kick a member and lets Discord delete all his messages up to 7 days old. Permissions for using this command are the same as for the kick @@ -718,15 +736,15 @@ async def user_can_kick(user): caseID = "'Unsaved'" if self.bot.database_online: Cases = self.bot.get_cog('Cases') - case = Case(bot=self.bot,guildID=ctx.guild.id,memberID=user.id,Type="softban",ModID=ctx.author.id,Reason=reason,date=ctx.bot.utcnow()) + case = Case(bot=self.bot,guild_id=ctx.guild.id,member_id=user.id,case_type="softban",mod_id=ctx.author.id,reason=reason,date=ctx.bot.utcnow()) try: await Cases.add_case(case) caseID = case.id - except Exception as e: - await self.bot.get_cog('Errors').on_error(e,ctx) + except Exception as err: + self.bot.dispatch("error", err, ctx) try: await ctx.message.delete() - except discord.Forbidden: + except (discord.Forbidden, discord.NotFound): pass # optional values opt_case = None if caseID=="'Unsaved'" else caseID @@ -737,9 +755,9 @@ async def user_can_kick(user): await self.send_modlogs("softban", user, ctx.author, ctx.guild, opt_case, opt_reason) except discord.errors.Forbidden: await ctx.send(await self.bot._(ctx.guild.id, "moderation.kick.too-high")) - except Exception as e: + except Exception as err: await ctx.send(await self.bot._(ctx.guild.id, "moderation.error")) - await self.bot.get_cog('Errors').on_error(e,ctx) + self.bot.dispatch("error", err, ctx) async def dm_user(self, user: discord.User, action: str, ctx: MyContext, reason: str = None, duration: str = None): if user.id in self.bot.get_cog('Welcomer').no_message: @@ -765,15 +783,15 @@ async def dm_user(self, user: discord.User, action: str, ctx: MyContext, reason: value=reason, inline=False) try: await user.send(embed=emb) - except discord.Forbidden: + except (discord.Forbidden, discord.NotFound): pass - except discord.HTTPException as e: - if e.code == 50007: + except discord.HTTPException as err: + if err.code == 50007: # "Cannot send message to this user" return - await self.bot.get_cog('Errors').on_error(e, ctx) - except Exception as e: - await self.bot.get_cog('Errors').on_error(e, ctx) + self.bot.dispatch("error", err, ctx) + except Exception as err: + self.bot.dispatch("error", err, ctx) async def send_chat_answer(self, action: str, user: discord.User, ctx: MyContext, case: int = None): if action in ('warn', 'mute', 'unmute', 'kick', 'ban', 'unban'): @@ -815,13 +833,15 @@ async def send_modlogs(self, action: str, user: discord.User, author: discord.Us fields.append({'name': _reason.capitalize(), 'value': reason}) await self.bot.get_cog("Events").send_logs_per_server(guild, action, message, author, fields) - @commands.command(name="banlist") + @commands.hybrid_command(name="banlist") + @app_commands.default_permissions(ban_members=True) + @app_commands.describe(show_reasons="Show or not the bans reasons") @commands.guild_only() @commands.check(checks.has_admin) @commands.check(checks.bot_can_embed) - async def banlist(self, ctx: MyContext, reasons:bool=True): + async def banlist(self, ctx: MyContext, show_reasons: bool=True): """Check the list of currently banned members. -The 'reasons' parameter is used to display the ban reasons. +The 'show_reasons' parameter is used to display the ban reasons. You must be an administrator of this server to use this command. @@ -834,19 +854,16 @@ class BansPaginator(Paginator): "Paginator used to display banned users" saved_bans: list[discord.guild.BanEntry] = [] users: set[int] = set() - async def send_init(self, ctx: MyContext): - "Create and send 1st page" - contents = await self.get_page_content(None, 1) - await self._update_buttons(None) - await ctx.send(**contents, view=self) - async def get_page_count(self, _: discord.Interaction) -> int: + + async def get_page_count(self, interaction) -> int: length = len(self.saved_bans) if length == 0: return 1 if length%1000 == 0: return self.page+1 return ceil(length/30) - async def get_page_content(self, interaction: discord.Interaction, page: int): + + async def get_page_content(self, interaction, page): "Create one page" if last_user := (None if len(self.saved_bans) == 0 else self.saved_bans[-1].user): self.saved_bans += [ @@ -866,7 +883,7 @@ async def get_page_content(self, interaction: discord.Interaction, page: int): page_start, page_end = (page-1)*30, min(page*30, len(self.saved_bans)) for i in range(page_start, page_end, 10): column_start, column_end = i+1, min(i+10, len(self.saved_bans)) - if reasons: + if show_reasons: values = [f"{entry.user} *({entry.reason})*" for entry in self.saved_bans[i:i+10]] else: values = [str(entry.user) for entry in self.saved_bans[i:i+10]] @@ -882,58 +899,112 @@ async def get_page_content(self, interaction: discord.Interaction, page: int): await view.send_init(ctx) - @commands.command(name="mutelist") + @commands.hybrid_command(name="mutelist") + @app_commands.default_permissions(moderate_members=True) + @app_commands.describe(show_reasons="Show or not the mute reasons") @commands.guild_only() @commands.check(checks.can_mute) - async def mutelist(self, ctx: MyContext, reasons:bool=True): - """Check the list of currently muted members. -The 'reasons' parameter is used to display the mute reasons. + async def mutelist(self, ctx: MyContext, show_reasons:bool=True): + """Check the list of members currently **muted by using this bot**. +The 'show_reasons' parameter is used to display the mute reasons. ..Doc moderator.html#banlist-mutelist""" try: - liste = await self.bdd_muted_list(ctx.guild.id, reasons=reasons) - except Exception as e: + muted_list = await self.bdd_muted_list(ctx.guild.id, reasons=show_reasons) + except Exception as err: await ctx.send(await self.bot._(ctx.guild.id, "moderation.error")) - await self.bot.get_cog("Errors").on_command_error(ctx, e) + self.bot.dispatch("error", err, ctx) return - desc = list() - title = await self.bot._(ctx.guild.id, "moderation.mute.list-title-0", guild=ctx.guild.name) - if len(liste) == 0: - desc.append(await self.bot._(ctx.guild.id, "moderation.mute.no-mutes")) - elif reasons: - _unknown = (await self.bot._(ctx.guild, "misc.unknown")).capitalize() - for userid, reason in liste.items(): - user: Optional[discord.User] = self.bot.get_user(userid) - if user is None: - continue - if len(desc) >= 45: - break - _reason = reason if ( - reason is not None and reason != "Unspecified") else _unknown - desc.append("{} *({})*".format(user, _reason)) - if len(liste) > 45: # overwrite title with limit - title = await self.bot._(ctx.guild.id, "moderation.mute.list-title-1", guild=ctx.guild.name) - else: - for userid in liste[:45]: - user: Optional[discord.User] = self.bot.get_user(userid) - if user is None: - continue - if len(desc) >= 60: - break - desc.append(str(user)) - if len(liste) > 60: # overwrite title with limit - title = await self.bot._(ctx.guild.id, "moderation.mute.list-title-2", guild=ctx.guild.name) - embed = discord.Embed(title=title, color=self.bot.get_cog("Servers").embed_color, - description="\n".join(desc), timestamp=ctx.message.created_at) - embed.set_footer(text=ctx.author, icon_url=ctx.author.display_avatar) - try: - await ctx.send(embed=embed, delete_after=20) - except discord.errors.HTTPException as e: - if e.code == 400: - await ctx.send(await self.bot._(ctx.guild.id, "moderation.ban.list-error")) + class MutesPaginator(Paginator): + "Paginator used to display muted users" + users_map: dict[int, Optional[discord.User]] = {} - @commands.group(name="emoji",aliases=['emojis', 'emote']) + async def get_page_count(self, interaction) -> int: + length = len(muted_list) + if length == 0: + return 1 + return ceil(length/30) + + async def _resolve_user(self, user_id: int): + if user := self.users_map.get(user_id): + return user + if user := self.client.get_user(user_id): + self.users_map[user_id] = user + return user + if user := await self.client.fetch_user(user_id): + self.users_map[user_id] = user + return user + return user_id + + async def get_page_content(self, interaction, page): + "Create one page" + title = await self.client._(ctx.guild.id, "moderation.mute.list-title-0", guild=ctx.guild.name) + emb = discord.Embed(title=title, color=self.client.get_cog("Servers").embed_color) + if len(muted_list) == 0: + emb.description = await self.client._(ctx.guild.id, "moderation.mute.no-mutes") + else: + page_start, page_end = (page-1)*30, min(page*30, len(muted_list)) + for i in range(page_start, page_end, 10): + column_start, column_end = i+1, min(i+10, len(muted_list)) + values: list[str] = [] + if show_reasons: + for user_id, reason in list(muted_list.items())[i:i+10]: + user = await self._resolve_user(user_id) + values.append(f"{user} *({reason})*") + else: + for user_id in muted_list[i:i+10]: + user = await self._resolve_user(user_id) + values.append(str(user)) + emb.add_field(name=f"{column_start}-{column_end}", value="\n".join(values)) + footer = f"{ctx.author} | {page}/{await self.get_page_count(interaction)}" + emb.set_footer(text=footer, icon_url=ctx.author.display_avatar) + return { + "embed": emb + } + + _quit = await self.bot._(ctx.guild, "misc.quit") + view = MutesPaginator(self.bot, ctx.author, stop_label=_quit.capitalize()) + await view.send_init(ctx) + + # desc = list() + + # if len(muted_list) == 0: + # desc.append(await self.bot._(ctx.guild.id, "moderation.mute.no-mutes")) + # elif show_reasons: + # _unknown = (await self.bot._(ctx.guild, "misc.unknown")).capitalize() + # for userid, reason in muted_list.items(): + # user: Optional[discord.User] = self.bot.get_user(userid) + # if user is None: + # continue + # if len(desc) >= 45: + # break + # _reason = reason if ( + # reason is not None and reason != "Unspecified") else _unknown + # desc.append("{} *({})*".format(user, _reason)) + # if len(muted_list) > 45: # overwrite title with limit + # title = await self.bot._(ctx.guild.id, "moderation.mute.list-title-1", guild=ctx.guild.name) + # else: + # for userid in muted_list[:45]: + # user: Optional[discord.User] = self.bot.get_user(userid) + # if user is None: + # continue + # if len(desc) >= 60: + # break + # desc.append(str(user)) + # if len(muted_list) > 60: # overwrite title with limit + # title = await self.bot._(ctx.guild.id, "moderation.mute.list-title-2", guild=ctx.guild.name) + # embed = discord.Embed(title=title, color=self.bot.get_cog("Servers").embed_color, + # description="\n".join(desc), timestamp=ctx.message.created_at) + # try: + # await ctx.send(embed=embed) + # except discord.errors.HTTPException as err: + # if err.code == 400: + # await ctx.send(await self.bot._(ctx.guild.id, "moderation.ban.list-error")) + + + @commands.hybrid_group(name="emoji",aliases=['emojis', 'emote']) + @app_commands.default_permissions(manage_emojis=True) @commands.guild_only() @commands.cooldown(5,20, commands.BucketType.guild) async def emoji_group(self, ctx: MyContext): @@ -942,14 +1013,16 @@ async def emoji_group(self, ctx: MyContext): ..Doc moderator.html#emoji-manager""" if ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx,['emoji']) + await ctx.send_help(ctx.command) @emoji_group.command(name="rename") + @app_commands.describe(emoji="The emoji to rename", name="The new name") + @commands.guild_only() @commands.check(checks.has_manage_emojis) async def emoji_rename(self, ctx: MyContext, emoji: discord.Emoji, name: str): """Rename an emoji - ..Example emoji rename :cool: supercool + ..Example emoji rename :cool\: supercool ..Doc moderator.html#emoji-manager""" if emoji.guild != ctx.guild: @@ -962,6 +1035,8 @@ async def emoji_rename(self, ctx: MyContext, emoji: discord.Emoji, name: str): await ctx.send(await self.bot._(ctx.guild.id, "moderation.emoji.renamed", emoji=emoji)) @emoji_group.command(name="restrict") + @app_commands.describe(emoji="The emoji to restrict", roles="The roles allowed to use this emoji (separated by spaces), or 'everyone'") + @commands.guild_only() @commands.check(checks.has_manage_emojis) async def emoji_restrict(self, ctx: MyContext, emoji: discord.Emoji, roles: commands.Greedy[Union[discord.Role, Literal['everyone']]]): """Restrict the use of an emoji to certain roles @@ -986,6 +1061,7 @@ async def emoji_restrict(self, ctx: MyContext, emoji: discord.Emoji, roles: comm await ctx.send(await self.bot._(ctx.guild.id, "moderation.emoji.emoji-valid", name=emoji, roles=", ".join([x.name for x in roles]))) @emoji_group.command(name="clear") + @commands.guild_only() @commands.check(checks.has_manage_msg) async def emoji_clear(self, ctx: MyContext, message: discord.Message, emoji: discord.Emoji = None): """Remove all reactions under a message @@ -1004,24 +1080,13 @@ async def emoji_clear(self, ctx: MyContext, message: discord.Message, emoji: dis await message.clear_reactions() try: await ctx.message.delete() - except discord.Forbidden: + except (discord.Forbidden, discord.NotFound): pass - @emoji_group.command(name="info") - @commands.check(checks.has_manage_emojis) - async def emoji_info(self, ctx: MyContext, emoji: discord.Emoji): - """Get info about an emoji - This is only an alias or `info emoji` - - ..Example info :owo:""" - msg = copy.copy(ctx.message) - msg.content = ctx.prefix + "info emoji " + str(emoji.id) - new_ctx = await self.bot.get_context(msg) - await self.bot.invoke(new_ctx) - @emoji_group.command(name="list") + @commands.guild_only() @commands.check(checks.bot_can_embed) - async def emoji_list(self, ctx: MyContext, page: int = 1): + async def emoji_list(self, ctx: MyContext): """List every emoji of your server ..Example emojis list @@ -1029,50 +1094,67 @@ async def emoji_list(self, ctx: MyContext, page: int = 1): ..Example emojis list 2 ..Doc moderator.html#emoji-manager""" - if page < 1: - await ctx.send(await self.bot._(ctx.guild.id, "xp.low-page")) - return structure = await self.bot._(ctx.guild.id, "moderation.emoji.list") - date = FormatUtils.date - lang = await self.bot._(ctx.guild.id,'_used_locale') priv = "**"+await self.bot._(ctx.guild.id, "moderation.emoji.private")+"**" title = await self.bot._(ctx.guild.id, "moderation.emoji.list-title", guild=ctx.guild.name) - try: - emotes = [structure.format(x,x.name,await date(x.created_at,lang,year=True,hour=False,digital=True),priv if len(x.roles) > 0 else '') for x in ctx.guild.emojis if not x.animated] - emotes += [structure.format(x,x.name,await date(x.created_at,lang,year=True,hour=False,digital=True),priv if len(x.roles) > 0 else '') for x in ctx.guild.emojis if x.animated] - if (page-1)*50 >= len(emotes): - await ctx.send(await self.bot._(ctx.guild.id, "xp.high-page")) - return - emotes = emotes[(page-1)*50:page*50] - nbr = len(emotes) - embed = discord.Embed(title=title, color=self.bot.get_cog('Servers').embed_color) - for i in range(0, min(50, nbr), 10): - emotes_list = list() - for emote in emotes[i:i+10]: - emotes_list.append(emote) - field_name = "{}-{}".format(i+1, i+10 if i+10 < nbr else nbr) - embed.add_field(name=field_name, value="\n".join(emotes_list), inline=False) - embed.set_footer(text=ctx.author, icon_url=ctx.author.display_avatar) - await ctx.send(embed=embed) - except Exception as e: - await ctx.bot.get_cog('Errors').on_command_error(ctx,e) - - - @commands.group(name="role", aliases=["roles"]) + # static emojis + emotes = [ + structure.format(x, x.name, f"", priv if len(x.roles) > 0 else '') + for x in ctx.guild.emojis + if not x.animated + ] + # animated emojis + emotes += [ + structure.format(x, x.name, f"", priv if len(x.roles) > 0 else '') + for x in ctx.guild.emojis + if x.animated + ] + + class EmojisPaginator(Paginator): + async def get_page_count(self, _: discord.Interaction) -> int: + length = len(emotes) + if length == 0: + return 1 + return ceil(length / 50) + + async def get_page_content(self, _: discord.Interaction, page: int): + "Create one page" + first_index = (page - 1) * 50 + last_index = min(first_index + 50, len(emotes)) + embed = discord.Embed(title=title, color=ctx.bot.get_cog('Servers').embed_color) + for i in range(first_index, last_index, 10): + emotes_list = list() + for emote in emotes[i:i+10]: + emotes_list.append(emote) + field_name = "{}-{}".format(i + 1, i + len(emotes_list)) + embed.add_field(name=field_name, value="\n".join(emotes_list), inline=False) + return { + "embed": embed + } + + _quit = await self.bot._(ctx.guild, "misc.quit") + view = EmojisPaginator(self.bot, ctx.author, stop_label=_quit.capitalize()) + await view.send_init(ctx) + + + @commands.hybrid_group(name="role", aliases=["roles"]) + @app_commands.default_permissions(manage_roles=True) @commands.guild_only() async def main_role(self, ctx: MyContext): """A few commands to manage roles ..Doc moderator.html#emoji-manager""" - if ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx,['role']) + if ctx.subcommand_passed is None and ctx.interaction is None: + await ctx.send_help(ctx.command) - @main_role.command(name="color",aliases=['colour']) + @main_role.command(name="set-color", aliases=['set-colour']) + @app_commands.describe(color="The new color role, preferably in hex format (#ff6699)") + @commands.guild_only() @commands.check(checks.has_manage_roles) async def role_color(self, ctx: MyContext, role: discord.Role, color: discord.Color): """Change a color of a role - ..Example role color "Admin team" red + ..Example role set-color "Admin team" red ..Doc moderator.html#role-manager""" if not ctx.guild.me.guild_permissions.manage_roles: @@ -1084,12 +1166,14 @@ async def role_color(self, ctx: MyContext, role: discord.Role, color: discord.Co await role.edit(colour=color,reason="Asked by {}".format(ctx.author)) await ctx.send(await self.bot._(ctx.guild.id,"moderation.role.color-success", role=role.name)) - @main_role.command(name="list") - @commands.cooldown(5,30,commands.BucketType.guild) + @main_role.command(name="members-list") + @commands.cooldown(5, 30, commands.BucketType.guild) + @commands.guild_only() + @commands.check(checks.has_manage_roles) async def role_list(self, ctx: MyContext, *, role: discord.Role): """Send the list of members in a role - ..Example role list "Technical team" + ..Example role members-list "Technical team" ..Doc moderator.html#role-manager""" if not (await checks.has_manage_roles(ctx) or await checks.has_manage_guild(ctx) or await checks.has_manage_msg(ctx)): @@ -1114,62 +1198,18 @@ async def role_list(self, ctx: MyContext, *, role: discord.Role): emb.set_footer(text=ctx.author, icon_url=ctx.author.display_avatar) await ctx.send(embed=emb) - @main_role.command(name="server-list",aliases=["glist"]) - @commands.check(checks.bot_can_embed) - @commands.cooldown(5,30,commands.BucketType.guild) - async def role_glist(self, ctx:MyContext): - """Check the list of every role - - ..Example role glist - - ..Doc moderator.html#role-manager""" - if not (await checks.has_manage_roles(ctx) or await checks.has_manage_guild(ctx) or await checks.has_manage_msg(ctx)): - await ctx.send(await self.bot._(ctx.guild.id, "moderation.missing-user-perms")) - return - tr_mbr = await self.bot._(ctx.guild.id,"misc.membres") - title = await self.bot._(ctx.guild.id, "moderation.role.list") - desc = list() - count = 0 - for role in ctx.guild.roles[1:]: - txt = "{} - {} {}".format(role.mention, len(role.members), tr_mbr) - if count+len(txt) > 2040: - emb = discord.Embed(title=title, description="\n".join(desc), color=ctx.guild.me.color, timestamp=ctx.message.created_at) - emb.set_footer(text=ctx.author, icon_url=ctx.author.display_avatar) - await ctx.send(embed=emb) - desc.clear() - count = 0 - desc.append(txt) - count += len(txt)+2 - if count > 0: - emb = discord.Embed(title=title, description="\n".join(desc), color=ctx.guild.me.color, timestamp=ctx.message.created_at) - emb.set_footer(text=ctx.author, icon_url=ctx.author.display_avatar) - await ctx.send(embed=emb) - - - @main_role.command(name="info") - @commands.check(checks.has_manage_msg) - async def role_info(self, ctx: MyContext, role:discord.Role): - """Get info about a role - This is only an alias or `info role` - ..Example role info VIP+ - - ..Doc moderator.html#role-manager""" - msg = copy.copy(ctx.message) - msg.content = ctx.prefix + "info role " + str(role.id) - new_ctx = await self.bot.get_context(msg) - await self.bot.invoke(new_ctx) - - @main_role.command(name="give", aliases=["add", "grant"]) - @commands.check(checks.has_manage_roles) + @main_role.command(name="grant", aliases=["add", "give"]) @commands.cooldown(1, 30, commands.BucketType.guild) + @commands.guild_only() + @commands.check(checks.has_manage_roles) async def roles_give(self, ctx:MyContext, role:discord.Role, users:commands.Greedy[Union[discord.Role,discord.Member,Literal['everyone']]]): """Give a role to a list of roles/members Users list may be either members or roles, or even only one member - ..Example role give Elders everyone + ..Example role grant Elders everyone - ..Example role give Slime Theo AsiliS + ..Example role grant Slime Theo AsiliS ..Doc moderator.html#role-manager""" if len(users) == 0: @@ -1208,14 +1248,15 @@ async def roles_give(self, ctx:MyContext, role:discord.Role, users:commands.Gree else: await ctx.send(answer) - @main_role.command(name="remove", aliases=["revoke"]) - @commands.check(checks.has_manage_roles) + @main_role.command(name="revoke", aliases=["remove"]) @commands.cooldown(1, 30, commands.BucketType.guild) + @commands.guild_only() + @commands.check(checks.has_manage_roles) async def roles_remove(self, ctx:MyContext, role:discord.Role, users:commands.Greedy[Union[discord.Role,discord.Member,Literal['everyone']]]): """Remove a role to a list of roles/members Users list may be either members or roles, or even only one member - ..Example role remove VIP @muted + ..Example role revoke VIP @muted ..Doc moderator.html#role-manager""" if len(users) == 0: @@ -1231,8 +1272,8 @@ async def roles_remove(self, ctx:MyContext, role:discord.Role, users:commands.Gr for item in users: if item == "everyone": item = ctx.guild.default_role - if isinstance(item,discord.Member): - if role not in item.roles: + if isinstance(item, discord.Member): + if role in item.roles: n_users.add(item) else: for m in item.members: @@ -1254,33 +1295,13 @@ async def roles_remove(self, ctx:MyContext, role:discord.Role, users:commands.Gr else: await ctx.send(answer) - - @commands.command(name="pin") - @commands.check(checks.has_manage_msg) - async def pin_msg(self, ctx: MyContext, msg: int): - """Pin a message -ID corresponds to the Identifier of the message - -..Example pin https://discord.com/channels/159962941502783488/201215818724409355/505373568184483851""" - if ctx.guild is not None and not ctx.channel.permissions_for(ctx.guild.me).manage_messages: - await ctx.send(await self.bot._(ctx.channel, "moderation.cant-pin")) - return - try: - message = await ctx.channel.fetch_message(msg) - except Exception as e: - await ctx.send(await self.bot._(ctx.channel, "moderation.pin.error-notfound", err=e)) - return - try: - await message.pin() - except Exception as e: - await ctx.send(await self.bot._(ctx.channel, "moderation.pin.error-toomuch", err=e)) - return - - @commands.command(name='unhoist') + @commands.hybrid_command(name='unhoist') + @app_commands.default_permissions(manage_nicknames=True) + @app_commands.describe(chars="The list of characters that should be considered as hoisting") @commands.guild_only() @commands.check(checks.has_manage_nicknames) async def unhoist(self, ctx: MyContext, chars: str=None): - """Remove the special characters from usernames + """Remove the special characters from the beginning of usernames ..Example unhoist @@ -1289,19 +1310,23 @@ async def unhoist(self, ctx: MyContext, chars: str=None): ..Doc moderator.html#unhoist-members""" count = 0 if not ctx.channel.permissions_for(ctx.guild.me).manage_nicknames: - return await ctx.send(await self.bot._(ctx.guild.id,"moderation.missing-manage-nick")) + return await ctx.send(await self.bot._(ctx.guild.id, "moderation.missing-manage-nick")) + if len(ctx.guild.members) > 5000: + return await ctx.send(await self.bot._(ctx.guild.id, "moderation.unhoist-too-many-members")) if chars is None: def check(username: str): - while username < '0': + while username < '0' and len(username): username = username[1:] - if len(username) == 0: - username = "z unhoisted" + if len(username) == 0: + username = "z unhoisted" return username else: chars = chars.lower() - def check(username): - while username[0].lower() in chars+' ': + def check(username: str): + while len(username) and username[0].lower() in chars+' ': username = username[1:] + if len(username) == 0: + username = "z unhoisted" return username for member in ctx.guild.members: try: @@ -1314,36 +1339,38 @@ def check(username): pass await ctx.send(await self.bot._(ctx.guild.id,"moderation.unhoisted",count=count)) - @commands.command(name="destop") + @commands.hybrid_command(name="destop") + @app_commands.default_permissions(manage_messages=True) @commands.guild_only() @commands.check(checks.can_clear) @commands.cooldown(2, 30, commands.BucketType.channel) - async def destop(self, ctx:MyContext, message:discord.Message): - """Clear every message between now and another message + async def destop(self, ctx:MyContext, start_message:discord.Message): + """Clear every message between now and an older message Message can be either ID or url Limited to 1,000 messages ..Example destop https://discordapp.com/channels/356067272730607628/488769306524385301/740249890201796688 ..Doc moderator.html#clear""" - if message.guild != ctx.guild: + if start_message.guild != ctx.guild: await ctx.send(await self.bot._(ctx.guild.id, "moderation.destop.no-guild")) return - if not message.channel.permissions_for(ctx.guild.me).manage_messages: + if not start_message.channel.permissions_for(ctx.guild.me).manage_messages: await ctx.send(await self.bot._(ctx.guild.id, "moderation.need-manage-messages")) return - if not message.channel.permissions_for(ctx.guild.me).read_message_history: + if not start_message.channel.permissions_for(ctx.guild.me).read_message_history: await ctx.send(await self.bot._(ctx.guild.id, "moderation.need-read-history")) return - if message.created_at < ctx.bot.utcnow() - datetime.timedelta(days=21): + if start_message.created_at < ctx.bot.utcnow() - datetime.timedelta(days=21): await ctx.send(await self.bot._(ctx.guild.id, "moderation.destop.too-old", days=21)) return - messages = await message.channel.purge(after=message, limit=1000, oldest_first=False) - await message.delete() - messages.append(message) + await ctx.defer() + messages = await start_message.channel.purge(after=start_message, before=ctx.message, limit=1000, oldest_first=False) + await start_message.delete() + messages.append(start_message) txt = await self.bot._(ctx.guild.id, "moderation.clear.done", count=len(messages)) await ctx.send(txt, delete_after=2.0) - log = await self.bot._(ctx.guild.id,"logs.clear", channel=message.channel.mention, number=len(messages)) + log = await self.bot._(ctx.guild.id,"logs.clear", channel=start_message.channel.mention, number=len(messages)) await self.bot.get_cog("Events").send_logs_per_server(ctx.guild, "clear", log, ctx.author) @@ -1375,8 +1402,8 @@ async def configure_muted_role(self, guild: discord.Guild, role: discord.Role = count += 1 if category is not None and category.permissions_for(guild.me).manage_roles: await category.set_permissions(role, send_messages=False) - except Exception as e: - await self.bot.get_cog('Errors').on_error(e, None) + except Exception as err: + self.bot.dispatch("error", err) count = len(guild.channels) await self.bot.get_cog('Servers').modify_server(guild.id, values=[('muted_role',role.id)]) return role, count diff --git a/fcts/morpions.py b/fcts/morpions.py index a4936620..c71f6804 100644 --- a/fcts/morpions.py +++ b/fcts/morpions.py @@ -222,7 +222,7 @@ def check(msg: discord.Message): await self.bot.get_cog("Utilities").add_user_eventPoint(ctx.author.id, 8) await ctx.send(await self.display_grid(grille)+'\n'+resultat) except Exception as err: - await self.bot.get_cog('Errors').on_command_error(ctx, err) + self.bot.dispatch("command_error", ctx, err) async def setup(bot): diff --git a/fcts/partners.py b/fcts/partners.py index d3447d44..ecf2ffa1 100644 --- a/fcts/partners.py +++ b/fcts/partners.py @@ -36,7 +36,7 @@ async def db_get_partner(self, partnerID: int, guildID: int): liste = list(query_results) return liste except Exception as err: - await self.bot.get_cog('Errors').on_error(err,None) + self.bot.dispatch("error", err) async def db_get_guild(self, guildID: int): """Return every partners of a guild""" @@ -46,7 +46,7 @@ async def db_get_guild(self, guildID: int): liste = list(query_results) return liste except Exception as err: - await self.bot.get_cog('Errors').on_error(err,None) + self.bot.dispatch("error", err) async def db_get_partnered(self, invites: list): """Return every guilds which has this one as partner""" @@ -58,7 +58,7 @@ async def db_get_partnered(self, invites: list): liste = list(query_results) return liste except Exception as err: - await self.bot.get_cog('Errors').on_error(err,None) + self.bot.dispatch("error", err) async def db_set_partner(self,guildID:int,partnerID:str,partnerType:str,desc:str): """Add a partner into a server""" @@ -69,7 +69,7 @@ async def db_set_partner(self,guildID:int,partnerID:str,partnerType:str,desc:str pass return True except Exception as err: - await self.bot.get_cog('Errors').on_error(err,None) + self.bot.dispatch("error", err) return False async def db_edit_partner(self,partnerID:int,target:str=None,desc:str=None,msg:int=None): @@ -86,7 +86,7 @@ async def db_edit_partner(self,partnerID:int,target:str=None,desc:str=None,msg:i pass return True except Exception as err: - await self.bot.get_cog('Errors').on_error(err,None) + self.bot.dispatch("error", err) return False async def db_del_partner(self,partner_id:int): @@ -97,7 +97,7 @@ async def db_del_partner(self,partner_id:int): pass return True except Exception as e: - await self.bot.get_cog('Errors').on_error(e,None) + self.bot.dispatch("error", e) return False async def db_get_bot_guilds(self, bot_id: int) -> Optional[int]: @@ -139,7 +139,7 @@ async def get_bot_owners(self, bot_id:int, session:aiohttp.ClientSession) -> lis owners.append(o) return owners - async def update_partners(self, channel: discord.TextChannel, color: int =None) -> int: + async def update_partners(self, channel: discord.TextChannel, color: Optional[int] = None) -> int: """Update every partners of a channel""" if not channel.permissions_for(channel.guild.me).embed_links: return 0 @@ -157,8 +157,6 @@ async def update_partners(self, channel: discord.TextChannel, color: int =None) count = 0 if color is None: color = await self.bot.get_config(channel.guild.id,'partner_color') - if color is None: - color = self.bot.get_cog('Servers').default_opt['partner_color'] session = aiohttp.ClientSession(loop=self.bot.loop) for partner in partners: target_desc = partner['description'] @@ -181,13 +179,13 @@ async def update_partners(self, channel: discord.TextChannel, color: int =None) try: msg = await channel.fetch_message(partner['messageID']) await msg.edit(embed=emb) - except discord.errors.NotFound: + except (discord.errors.NotFound, discord.errors.Forbidden): msg = await channel.send(embed=emb) - await self.db_edit_partner(partnerID=partner['ID'],msg=msg.id) - except Exception as e: + await self.db_edit_partner(partnerID=partner['ID'], msg=msg.id) + except Exception as err: msg = await channel.send(embed=emb) - await self.db_edit_partner(partnerID=partner['ID'],msg=msg.id) - await self.bot.get_cog('Errors').on_error(e,None) + await self.db_edit_partner(partnerID=partner['ID'], msg=msg.id) + self.bot.dispatch("error", err) count += 1 await session.close() return count @@ -213,10 +211,10 @@ async def update_partner_bot(self, tr_bot: str, tr_guilds: str, tr_invite: str, image = usr.display_avatar.with_static_format("png") if usr else "" except discord.NotFound: title += "ID: "+partner['target'] - except Exception as e: + except Exception as err: usr = await self.bot.fetch_user(int(partner['target'])) image = usr.display_avatar.url if usr else "" - await self.bot.get_cog("Errors").on_error(e, None) + self.bot.dispatch("error", err) perm = discord.Permissions.all() perm.update(administrator=False) oauth_url = discord.utils.oauth_url(partner['target'], permissions=perm) @@ -276,7 +274,7 @@ async def partner_main(self, ctx: MyContext): ..Doc server.html#partners-system""" if ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx,['partner']) + await ctx.send_help(ctx.command) @partner_main.command(name='add') @commands.check(checks.database_connected) @@ -443,8 +441,6 @@ async def partner_list(self, ctx: MyContext): fields_name = await self.bot._(ctx.guild.id, "partners.partners-list") if ctx.can_send_embed: color = await ctx.bot.get_config(ctx.guild.id,'partner_color') - if color is None: - color = self.bot.get_cog('Servers').default_opt['partner_color'] emb = discord.Embed(title=fields_name[0], color=color, timestamp=self.bot.utcnow()) if ctx.guild.icon: emb.set_thumbnail(url=ctx.guild.icon) @@ -468,7 +464,7 @@ async def partner_color(self, ctx: MyContext, color): ..Doc server.html#change-the-embed-color""" await self.bot.get_cog('Servers').conf_color(ctx,'partner_color',str(color)) - + @partner_main.command(name='reload') @commands.check(checks.has_manage_guild) @commands.cooldown(1,60,commands.BucketType.guild) @@ -483,7 +479,7 @@ async def partner_reload(self, ctx: MyContext): chan: str = channel[0]['partner_channel'].split(';')[0] if not chan.isnumeric(): return await msg.edit(content=await self.bot._(ctx.guild, "partners.no-channel")) - chan = ctx.guild.get_channel(int(chan)) + chan = ctx.guild.get_channel_or_thread(int(chan)) if chan is None: return await msg.edit(content=await self.bot._(ctx.guild, "partners.no-channel")) count = await self.update_partners(chan,channel[0]['partner_color']) diff --git a/fcts/perms.py b/fcts/perms.py index e9c9c2aa..8aac5eea 100644 --- a/fcts/perms.py +++ b/fcts/perms.py @@ -1,6 +1,7 @@ import typing import discord +from discord import app_commands from discord.ext import commands from fcts.args import RawPermissionValue from libs.bot_classes import MyContext, Zbot @@ -15,12 +16,13 @@ TextChannelTypes = typing.Union[ discord.TextChannel, discord.CategoryChannel, + discord.ForumChannel, discord.Thread ] AcceptableChannelTypes = typing.Optional[typing.Union[ VoiceChannelTypes, - TextChannelTypes + TextChannelTypes, ]] AcceptableTargetTypes = typing.Optional[typing.Union[ discord.Member, @@ -28,6 +30,26 @@ RawPermissionValue ]] + +class TargetConverter(commands.Converter): + async def convert(self, ctx: MyContext, argument: str) -> AcceptableTargetTypes: + try: + return await commands.MemberConverter().convert(ctx, argument) + except commands.MemberNotFound: + pass + + try: + return await commands.RoleConverter().convert(ctx, argument) + except commands.RoleNotFound: + pass + + try: + return await RawPermissionValue().convert(ctx, argument) + except commands.BadArgument: + pass + + raise commands.BadArgument(f"Could not find a member, role or permission value with the name {argument}") + class Perms(commands.Cog): """Cog with a single command, allowing you to see the permissions of a member or a role in a channel.""" @@ -72,18 +94,20 @@ async def collect_permissions(self, ctx: MyContext, permissions: discord.Permiss result.append((emojis_cog.customs['red_cross'], perm_tr)) return result - @commands.command(name='perms', aliases=['permissions']) + @commands.hybrid_command(name='permissions', aliases=['perms']) + @app_commands.default_permissions(manage_roles=True) + @app_commands.describe(channel="The channel to check the permissions in", target="The member or role to check the permissions of, or an integer/binary value") @commands.guild_only() - async def check_permissions(self, ctx: MyContext, channel:AcceptableChannelTypes=None, *, target:AcceptableTargetTypes=None): - """Permissions assigned to a member/role (the user by default) - The channel used to view permissions is the channel in which the command is entered. + async def check_permissions(self, ctx: MyContext, channel:AcceptableChannelTypes=None, *, target: typing.Annotated[AcceptableTargetTypes, TargetConverter]=None): + """Check the permissions assigned to a member/role + By default, it will calculate the author's permissions at the server level. You can also choose to view the permissions associated to a raw integer/binary value (in which case channel will be ignored) - ..Example perms #announcements everyone + ..Example permissions #announcements everyone - ..Example perms Zbot + ..Example permissions Zbot - ..Example perms 0b1001 + ..Example permissions 0b1001 ..Doc infos.html#permissions""" if ctx.current_argument and target is None and channel is None: @@ -98,7 +122,7 @@ async def check_permissions(self, ctx: MyContext, channel:AcceptableChannelTypes perms = channel.permissions_for(target) col = target.color avatar = target.display_avatar.replace(static_format="png", size=256) - name = str(target) + name = await self.bot._(ctx, "permissions.target.member", name=str(target)) elif isinstance(target, discord.Role): if channel is None: perms = target.permissions @@ -106,12 +130,12 @@ async def check_permissions(self, ctx: MyContext, channel:AcceptableChannelTypes perms = channel.permissions_for(target) col = target.color avatar = ctx.guild.icon.replace(format='png', size=256) if ctx.guild.icon else None - name = str(target) + name = await self.bot._(ctx, "permissions.target.role", name=str(target)) elif isinstance(target, int): perms = discord.Permissions(target) col = discord.Color.blurple() avatar = None - name = f"{target} | {bin(target)}" + name = await self.bot._(ctx, "permissions.target.value", value=f"{target} | {bin(target)}") else: self.bot.dispatch("error", TypeError(f"Unknown target type: {type(target)}"), ctx) return @@ -120,16 +144,21 @@ async def check_permissions(self, ctx: MyContext, channel:AcceptableChannelTypes perms_list.sort(key=lambda x: x[1]) perms_list = [''.join(perm) for perm in perms_list] if ctx.can_send_embed: - if channel is None: - desc = await self.bot._(ctx.guild.id, "permissions.general") + if isinstance(target, int): + desc = None + elif channel is None: + desc = await self.bot._(ctx, "permissions.channel.general") + elif isinstance(channel, discord.CategoryChannel): + desc = await self.bot._(ctx, "permissions.channel.category", name=channel.name) else: - desc = channel.mention + desc = await self.bot._(ctx, "permissions.channel.channel", mention=channel.mention) + embed = discord.Embed(color=col, description=desc) paragraphs = cut_text(perms_list, max_size=21) for paragraph in paragraphs: embed.add_field(name=self.bot.zws, value=paragraph) - _whatisthat = await self.bot._(ctx.guild.id, "permissions.whatisthat") + _whatisthat = await self.bot._(ctx, "permissions.whatisthat") embed.add_field(name=self.bot.zws, value=f'[{_whatisthat}](https://zbot.readthedocs.io/en/latest/perms.html)', inline=False) embed.set_author(name=name, icon_url=avatar) diff --git a/fcts/reloads.py b/fcts/reloads.py index 0b8a92a4..a6ff9f5f 100644 --- a/fcts/reloads.py +++ b/fcts/reloads.py @@ -10,10 +10,16 @@ class Reloads(commands.Cog): def __init__(self, bot: Zbot): self.bot = bot self.file = "reloads" - self.ignored_guilds = [471361000126414848,513087032331993090,500648624204808193,264445053596991498,446425626988249089,707248438391078978] - + self.ignored_guilds = [ + 471361000126414848, # Zbot emojis 1 + 513087032331993090, # Zbot emojis 2 + 500648624204808193, # Emergency server + 446425626988249089, # Bots on Discord + 707248438391078978, # ? + 568567800910839811, # Delly + ] + async def reload_cogs(self, ctx: MyContext, cogs: list[str]): - errors_cog = self.bot.get_cog("Errors") if len(cogs)==1 and cogs[0]=='all': cogs = sorted([x.file for x in self.bot.cogs.values()]) reloaded_cogs = list() @@ -36,7 +42,7 @@ async def reload_cogs(self, ctx: MyContext, cogs: list[str]): self.bot.log.info(f"Lib {cog} reloaded") await ctx.send(f"Lib {cog} reloaded") except Exception as err: - await errors_cog.on_error(err,ctx) + self.bot.dispatch("error", err, ctx) await ctx.send(f'**`ERROR:`** {type(err).__name__} - {err}') else: self.bot.log.info(f"Module {cog} rechargé") diff --git a/fcts/roles_react.py b/fcts/roles_react.py index 973e5dfa..bb3422d1 100644 --- a/fcts/roles_react.py +++ b/fcts/roles_react.py @@ -40,7 +40,7 @@ async def prepare_react(self, payload: discord.RawReactionActionEvent) -> Tuple[ except Exception as e: self.bot.log.warning(f"Could not fetch roles-reactions message {payload.message_id} in guild {payload.guild_id}: {e}") return None, None - if len(msg.embeds) == 0 or msg.embeds[0].footer.text != self.footer_txt or msg.author.id != self.bot.user.id: + if len(msg.embeds) == 0 or msg.embeds[0].footer.text != self.footer_txt: return None, None temp = await self.rr_list_role(payload.guild_id, payload.emoji.id if payload.emoji.is_custom_emoji() else payload.emoji.name) if len(temp) == 0: @@ -119,7 +119,7 @@ async def rr_main(self, ctx): ..Doc roles-reactions.html""" if ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx, ['roles_react']) + await ctx.send_help(ctx.command) @rr_main.command(name="add") @commands.check(checks.has_manage_guild) @@ -141,12 +141,11 @@ async def rr_add(self, ctx: MyContext, emoji: args.AnyEmoji, role: discord.Role, if len(l) > 0: return await ctx.send(await self.bot._(ctx.guild.id, "roles_react.already-1-rr")) max_rr = await self.bot.get_config(ctx.guild.id, 'roles_react_max_number') - max_rr = self.bot.get_cog("Servers").default_opt['roles_react_max_number'] if max_rr is None else max_rr if len(l) >= max_rr: return await ctx.send(await self.bot._(ctx.guild.id, "roles_react.too-many-rr", l=max_rr)) await self.rr_add_role(ctx.guild.id, role.id, emoji, description[:150]) - except Exception as e: - await self.bot.get_cog('Errors').on_command_error(ctx, e) + except Exception as err: + self.bot.dispatch("command_error", ctx, err) else: await ctx.send(await self.bot._(ctx.guild.id, "roles_react.rr-added", r=role.name, e=emoji)) self.guilds_which_have_roles.add(ctx.guild.id) @@ -156,9 +155,9 @@ async def rr_add(self, ctx: MyContext, emoji: args.AnyEmoji, role: discord.Role, @commands.check(checks.has_manage_guild) async def rr_remove(self, ctx, emoji): """Remove a role react - + ..Example roles_react remove :uwu: - + ..Doc roles-reactions.html#add-and-remove-a-reaction""" try: # if emoji is a custom one: @@ -170,8 +169,8 @@ async def rr_remove(self, ctx, emoji): if len(l) == 0: return await ctx.send(await self.bot._(ctx.guild.id, "roles_react.no-rr")) await self.rr_remove_role(l[0]['ID']) - except Exception as e: - await self.bot.get_cog('Errors').on_command_error(ctx, e) + except Exception as err: + self.bot.dispatch("command_error", ctx, err) else: role = ctx.guild.get_role(l[0]['role']) if role is None: @@ -217,11 +216,10 @@ async def rr_list(self, ctx: MyContext): try: roles_list = await self.rr_list_role(ctx.guild.id) except Exception as err: - await self.bot.get_cog('Errors').on_command_error(ctx, err) + self.bot.dispatch("command_error", ctx, err) else: des, _ = await self.create_list_embed(roles_list, ctx.guild) max_rr = await self.bot.get_config(ctx.guild.id, 'roles_react_max_number') - max_rr = self.bot.get_cog("Servers").default_opt['roles_react_max_number'] if max_rr is None else max_rr title = await self.bot._(ctx.guild.id, "roles_react.rr-list", n=len(roles_list), m=max_rr) emb = discord.Embed(title=title, description=des, color=self.embed_color, timestamp=ctx.message.created_at) emb.set_footer(text=ctx.author, icon_url=ctx.author.display_avatar) @@ -238,7 +236,7 @@ async def rr_get(self, ctx: MyContext): try: roles_list = await self.rr_list_role(ctx.guild.id) except Exception as err: - await self.bot.get_cog('Errors').on_command_error(ctx, err) + self.bot.dispatch("command_error", ctx, err) else: des, emojis = await self.create_list_embed(roles_list, ctx.guild) title = await self.bot._(ctx.guild.id, "roles_react.rr-embed") @@ -304,7 +302,7 @@ async def give_remove_role(self, user: discord.Member, role: discord.Role, guild except discord.errors.Forbidden: pass except Exception as err: - await self.bot.get_cog('Errors').on_error(err, None) + self.bot.dispatch("error", err) else: if not ignore_success: await channel.send(await self.bot._(guild.id, "roles_react.role-given" if give else "roles_react.role-lost", r=role.name)) @@ -313,7 +311,7 @@ async def give_remove_role(self, user: discord.Member, role: discord.Role, guild @commands.check(checks.database_connected) async def rr_update(self, ctx: MyContext, embed: discord.Message, change_description: Optional[bool] = True, emojis: commands.Greedy[args.AnyEmoji] = None): """Update a Zbot message to refresh roles/reactions - If you don't want to update the embed content, for example if it's a custom embed, then you can use 'False' as a second argument. Zbot will only check the reactions + If you don't want to update the embed content (for example if it's a custom embed) then you can use 'False' as a second argument, and I will only check the reactions Specifying a list of emojis will update the embed only for those emojis, and ignore other roles reactions ..Example roles_react update https://discord.com/channels/356067272730607628/625320847296430120/707726569430319164 False @@ -331,7 +329,7 @@ async def rr_update(self, ctx: MyContext, embed: discord.Message, change_descrip try: full_list: dict[str, dict[str, Any]] = {x['emoji']: x for x in await self.rr_list_role(ctx.guild.id)} except Exception as err: - return await self.bot.get_cog('Errors').on_command_error(ctx, err) + return self.bot.dispatch("command_error", ctx, err) if emojis is not None: emojis_ids: list[str] = [str(x.id) if isinstance(x, discord.Emoji) else str(x) for x in emojis] diff --git a/fcts/rss.py b/fcts/rss.py index 9b863bd6..c5f493a9 100644 --- a/fcts/rss.py +++ b/fcts/rss.py @@ -54,7 +54,6 @@ def __init__(self, bot: Zbot): self.file = "rss" self.embed_color = discord.Color(6017876) self.loop_processing = False - self.last_update = None self.errors_treshold = 24 * 3 # max errors allowed before disabling a feed (24h) self.youtube_rss = YoutubeRSS(self.bot) @@ -89,7 +88,7 @@ async def rss_main(self, ctx: MyContext): ..Doc rss.html#rss""" if ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx,['rss']) + await ctx.send_help(ctx.command) @rss_main.command(name="youtube",aliases=['yt']) async def request_yt(self, ctx: MyContext, *, channel): @@ -160,7 +159,7 @@ async def request_tw(self, ctx: MyContext, name): try: text = await self.twitter_rss.get_feed(ctx.channel,name) except Exception as err: - return await self.bot.get_cog('Errors').on_error(err,ctx) + return self.bot.dispatch("error", err, ctx) if isinstance(text, str): await ctx.send(text) else: @@ -220,8 +219,6 @@ async def is_overflow(self, guild: discord.Guild) -> tuple[bool, int]: """Check if a guild still has at least a slot True if max number reached, followed by the feed limit""" feed_limit = await self.bot.get_config(guild.id,'rss_max_number') - if feed_limit is None: - feed_limit: int = self.bot.get_cog('Servers').default_opt['rss_max_number'] return len(await self.db_get_guild_feeds(guild.id)) >= feed_limit, feed_limit @rss_main.command(name="add") @@ -277,8 +274,9 @@ async def system_add(self, ctx: MyContext, link: str): self.bot.log.info("RSS feed added into server {} ({} - {})".format(ctx.guild.id,link,feed_id)) await self.send_log("Feed added into server {} ({})".format(ctx.guild.id,feed_id),ctx.guild) except Exception as err: - await ctx.send(await self.bot._(ctx.guild, "rss.fail-add")) - await self.bot.get_cog("Errors").on_error(err,ctx) + cmd = await self.bot.get_command_mention("about") + await ctx.send(await self.bot._(ctx.guild, "errors.unknown2", about=cmd)) + self.bot.dispatch("error", err, ctx) @rss_main.command(name="remove", aliases=["delete"]) @commands.guild_only() @@ -410,12 +408,14 @@ async def list_feeds(self, ctx: MyContext): embed.add_field(name=self.bot.zws, value=text, inline=False) await ctx.send(embed=embed) feeds_to_display.clear() + # last post date if isinstance(feed.date, datetime.datetime): last_date = f"" elif isinstance(feed.date, str): last_date = feed.date else: last_date = await self.bot._(ctx.guild.id, "misc.none") + # append data feeds_to_display.append(translation.format( emoji=feed.get_emoji(self.bot.emojis_manager), channel=channel, @@ -444,8 +444,8 @@ async def transform_feeds_to_options(self, feeds: list[FeedObject], guild: disco # formatted feed type name tr_type = await self.bot._(guild.id, "rss."+feed.type) # formatted channel - if channel := guild.get_channel(feed.channel_id): - tr_channel = "#"+channel.name + if channel := guild.get_channel_or_thread(feed.channel_id): + tr_channel = channel.mention else: tr_channel = "#deleted" # better name format (for Twitter/YouTube ID) @@ -505,11 +505,12 @@ async def ask_rss_id(self, input_id: Optional[int], ctx: MyContext, title:str, f await view.disable(msg) return try: - selection = list(map(int, view.values)) + selection = list(map(int, view.values)) if isinstance(view.values, list) else [int(view.values)] except ValueError: selection = [] if len(selection) == 0: - await ctx.send(await self.bot._(ctx.guild, "rss.fail-add")) + cmd = await self.bot.get_command_mention("about") + await ctx.send(await self.bot._(ctx.guild, "errors.unknown2", about=cmd)) return return selection @@ -548,12 +549,13 @@ async def roles_feeds(self, ctx: MyContext, ID:int=None, *, mentions: Optional[s ) except Exception as err: feeds_ids = [] - await self.bot.get_cog("Errors").on_error(err, ctx) + self.bot.dispatch("error", err, ctx) if feeds_ids is None: return feeds: list[FeedObject] = list(filter(None, [await self.db_get_feed(feed_id) for feed_id in feeds_ids])) if len(feeds) == 0: - await ctx.send(await self.bot._(ctx.guild, "rss.fail-add")) + cmd = await self.bot.get_command_mention("about") + await ctx.send(await self.bot._(ctx.guild, "errors.unknown2", about=cmd)) return no_role = {'aucun', 'none', '_', 'del'} if mentions is None: # if no roles was specified: we ask for them @@ -637,8 +639,9 @@ async def roles_feeds(self, ctx: MyContext, ID:int=None, *, mentions: Optional[s await self.db_update_feed(feed.feed_id, values=[('roles', ';'.join(roles_ids))]) await ctx.send(await self.bot._(ctx.guild.id, "rss.roles.edit-success", count=len(names), roles=", ".join(names))) except Exception as err: - await ctx.send(await self.bot._(ctx.guild, "rss.fail-add")) - await self.bot.get_cog("Errors").on_error(err, ctx) + cmd = await self.bot.get_command_mention("about") + await ctx.send(await self.bot._(ctx.guild, "errors.unknown2", about=cmd)) + self.bot.dispatch("error", err, ctx) return @@ -697,9 +700,10 @@ async def move_guild_feed(self, ctx:MyContext, ID:Optional[int]=None, channel:di if feeds_ids is None: return if len(feeds_ids) == 0: - await ctx.send(await self.bot._(ctx.guild, "rss.fail-add")) + cmd = await self.bot.get_command_mention("about") + await ctx.send(await self.bot._(ctx.guild, "errors.unknown2", about=cmd)) if err is not None: - await self.bot.get_cog("Errors").on_error(err,ctx) + self.bot.dispatch("error", err, ctx) return for feed in feeds_ids: await self.db_update_feed(feed, [('channel',channel.id)]) @@ -741,12 +745,13 @@ async def change_text_feed(self, ctx: MyContext, ID: Optional[int]=None, *, text ) except Exception as err: feeds_ids = [] - await self.bot.get_cog("Errors").on_error(err, ctx) + self.bot.dispatch("error", err, ctx) if feeds_ids is None: return feeds: list[FeedObject] = list(filter(None, [await self.db_get_feed(feed_id) for feed_id in feeds_ids])) if len(feeds) == 0: - await ctx.send(await self.bot._(ctx.guild, "rss.fail-add")) + cmd = await self.bot.get_command_mention("about") + await ctx.send(await self.bot._(ctx.guild, "errors.unknown2", about=cmd)) return if text is None: # if no text was specified: we ask for it @@ -794,13 +799,14 @@ async def change_use_embed(self, ctx: MyContext, feed_id: Optional[int] = None, ) except Exception as err: feeds_ids = [] - await self.bot.get_cog("Errors").on_error(err,ctx) + self.bot.dispatch("error", err, ctx) if feeds_ids is None: return if len(feeds_ids) == 0: - await ctx.send(await self.bot._(ctx.guild, "rss.fail-add")) + cmd = await self.bot.get_command_mention("about") + await ctx.send(await self.bot._(ctx.guild, "errors.unknown2", about=cmd)) if err is not None: - await self.bot.get_cog("Errors").on_error(err,ctx) + self.bot.dispatch("error", err, ctx) return if arguments is None or len(arguments.keys()) == 0: arguments = None @@ -841,7 +847,7 @@ def check(msg: discord.Message): await ctx.send("\n".join(txt)) except Exception as err: await ctx.send(await self.bot._(ctx.guild.id, "rss.guild-error", err=err)) - await ctx.bot.get_cog('Errors').on_error(err, ctx) + self.bot.dispatch("error", err, ctx) @rss_main.command(name="test") @commands.check(checks.is_support_staff) @@ -1190,7 +1196,7 @@ async def db_add_feed(self, guild_id:int, channel_id:int, _type:str, link:str): form = '' else: form = await self.bot._(guild_id, f"rss.{_type}-default-flow") - query = "INSERT INTO `{}` (`ID`, `guild`,`channel`,`type`,`link`,`structure`) VALUES (%(i)s,%(g)s,%(c)s,%(t)s,%(l)s,%(f)s)".format(self.table) + query = "INSERT INTO `{}` (`ID`, `guild`, `channel`, `type`, `link`, `structure`) VALUES (%(i)s, %(g)s, %(c)s, %(t)s, %(l)s, %(f)s)".format(self.table) async with self.bot.db_query(query, { 'i': feed_id, 'g': guild_id, 'c': channel_id, 't': _type, 'l': link, 'f': form }): pass return feed_id @@ -1260,8 +1266,8 @@ async def db_increment_errors(self, working_ids: list[int], broken_ids: list[int async with self.bot.db_query(query, returnrowcount=True) as query_results: return query_results - async def db_set_active_guilds(self, active_guild_ids: list[int]) -> int: - "Mark any guild in the list as an active guild, and every other as inactive (ie. the bot has no access to them anymore" + async def db_set_active_guilds(self, active_guild_ids: list[int]): + "DEPRECATED - Mark any guild in the list as an active guild, and every other as inactive (ie. the bot has no access to them anymore)" if self.bot.zombie_mode: return ids_list = ', '.join(map(str, active_guild_ids)) @@ -1270,7 +1276,17 @@ async def db_set_active_guilds(self, active_guild_ids: list[int]) -> int: self.bot.log.info("[rss] set guild as inactive for %s feeds", query_results) query = f"UPDATE `{self.table}` SET `active_guild` = 1 WHERE `guild` IN ({ids_list})" async with self.bot.db_query(query, returnrowcount=True) as query_results: - return query_results + if query_results: + self.bot.log.info("[rss] set guild as active for %s feeds", query_results) + + async def db_set_last_refresh(self, feed_ids: list[int]): + "Update the last_refresh field for the given feed IDs" + if self.bot.zombie_mode: + return + ids_list = ', '.join(map(str, feed_ids)) + query = f"UPDATE `{self.table}` SET `last_refresh` = %s WHERE `ID` IN ({ids_list})" + async with self.bot.db_query(query, (datetime.datetime.utcnow(),), returnrowcount=True) as query_results: + self.bot.log.info("[rss] set last refresh for %s feeds", query_results) async def send_rss_msg(self, obj: "RssMessage", channel: Union[discord.TextChannel, discord.Thread], roles: list[str], send_stats): "Send a RSS message into its Discord channel, with the corresponding mentions" @@ -1295,8 +1311,7 @@ async def send_rss_msg(self, obj: "RssMessage", channel: Union[discord.TextChann statscog.rss_stats['messages'] += 1 except discord.HTTPException as err: self.bot.log.info(f"[send_rss_msg] Cannot send message on channel {channel.id}: {err}") - await self.bot.get_cog("Errors").on_error(err) - await self.bot.get_cog("Errors").senf_err_msg(str(t.to_dict()) if hasattr(t, "to_dict") else str(t)) + self.bot.dispatch("error", err) except Exception as err: self.bot.log.info(f"[send_rss_msg] Cannot send message on channel {channel.id}: {err}") @@ -1338,7 +1353,7 @@ async def check_feed(self, feed: FeedObject, session: ClientSession = None, send for obj in objs[:self.max_messages]: # if the guild was marked as inactive (ie the bot wasn't there in the previous loop), # mark the feeds as completed but do not send any message, to avoid spamming channels - if feed.is_active_guild: + if feed.has_recently_been_refreshed(): # if we can't post messages: abort if not chan.permissions_for(guild.me).send_messages: self.bot.dispatch("server_warning", ServerWarningType.RSS_MISSING_TXT_PERMISSION, guild, channel=chan, feed_id=feed.feed_id) @@ -1358,7 +1373,7 @@ async def check_feed(self, feed: FeedObject, session: ClientSession = None, send else: return True except Exception as err: - error_msg = f"Erreur rss sur le flux {feed.link} (type {feed.type} - salon {feed.channel_id} - id {feed.feed_id})" + error_msg = f"Erreur rss sur le flux {feed.feed_id} (type {feed.type} - salon {feed.channel_id} - id {feed.feed_id})" self.bot.dispatch("error", err, error_msg) return False @@ -1424,7 +1439,8 @@ async def main_loop(self, guild_id: int=None): if statscog := self.bot.get_cog("BotStats"): statscog.rss_stats["checked"] = checked_count statscog.rss_stats["errors"] = len(errors_ids) - await self.db_set_active_guilds(set(feed.guild_id for feed in feeds_list)) + # await self.db_set_active_guilds(set(feed.guild_id for feed in feeds_list)) + await self.db_set_last_refresh(set(feed.feed_id for feed in feeds_list)) if len(errors_ids) > 0: desc.append(f"{len(errors_ids)} errors: {' '.join(str(x) for x in errors_ids)}") # update errors count in database @@ -1452,8 +1468,12 @@ async def loop_child(self): return self.bot.log.info(" Boucle rss commencée !") start_time = time.time() - await self.main_loop() - self.bot.log.info(f" Boucle rss terminée en {time.time() - start_time:.2f}s!") + try: + await self.main_loop() + except Exception as err: + self.bot.dispatch("error", err, "RSS main loop") + else: + self.bot.log.info(f" Boucle rss terminée en {time.time() - start_time:.2f}s!") @loop_child.before_loop async def before_printer(self): @@ -1463,7 +1483,7 @@ async def before_printer(self): @loop_child.error async def loop_error(self, error: Exception): "When the loop fails" - self.bot.dispatch("error", error, "RSS main loop") + self.bot.dispatch("error", error, "RSS main loop has stopped <@279568324260528128>") @commands.command(name="rss_loop",hidden=True) @@ -1501,7 +1521,7 @@ async def send_log(self, text: str, guild: discord.Guild): emb.set_author(name=self.bot.user, icon_url=self.bot.user.display_avatar) await self.bot.send_embed(emb) except Exception as err: - await self.bot.get_cog("Errors").on_error(err,None) + self.bot.dispatch("error", err) async def setup(bot): diff --git a/fcts/s_backups.py b/fcts/s_backups.py index 187da52f..311392b4 100644 --- a/fcts/s_backups.py +++ b/fcts/s_backups.py @@ -27,7 +27,7 @@ async def main_backup(self,ctx:MyContext): ..Doc server.html#server-backup""" if ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx,['backup']) + await ctx.send_help(ctx.command) @main_backup.command(name="load") @@ -102,7 +102,8 @@ async def backup_create(self,ctx:MyContext): ..Doc server.html#server-backup""" try: data = await self.create_backup(ctx) - await ctx.send(await self.bot._(ctx.guild.id, "s_backup.backup-done"),file=discord.File(BytesIO(data.encode()), filename=f"backup-{ctx.guild.id}.json")) + file = discord.File(BytesIO(data.encode()), filename=f"backup-{ctx.guild.id}.json") + await ctx.send(await self.bot._(ctx.guild.id, "s_backup.backup-done"), file=file) except Exception as e: await ctx.bot.get_cog('Errors').on_command_error(ctx,e) @@ -193,7 +194,7 @@ async def get_channel_json(chan) -> dict: except discord.errors.Forbidden: pass except Exception as err: - await ctx.bot.get_cog('Errors').on_error(err,ctx) + self.bot.dispatch("error", err, ctx) try: webs = [] for w in await g.webhooks(): @@ -207,7 +208,7 @@ async def get_channel_json(chan) -> dict: except discord.errors.Forbidden: pass except Exception as err: - await ctx.bot.get_cog('Errors').on_error(err,ctx) + self.bot.dispatch("error", err, ctx) back['members'] = [] for memb in g.members: back['members'].append({'id': memb.id, diff --git a/fcts/serverlogs.py b/fcts/serverlogs.py index 8c92d77c..471c7fc7 100644 --- a/fcts/serverlogs.py +++ b/fcts/serverlogs.py @@ -1,18 +1,19 @@ import asyncio import re -from typing import Any +from typing import Any, Optional import discord from cachingutils import LRUCache +from discord import app_commands from discord.ext import commands, tasks + +from fcts.args import serverlog +from fcts.tickets import TicketCreationEvent from libs.antiscam.classes import PredictionResult from libs.bot_classes import MyContext, Zbot from libs.enums import ServerWarningType from libs.formatutils import FormatUtils -from fcts.args import serverlog -from fcts.tickets import TicketCreationEvent - from . import checks DISCORD_INVITE = re.compile(r'(?:https?://)?(?:www[.\s])?((?:discord[.\s](?:gg|io|me|li(?:nk)?)|discordapp\.com/invite|discord\.com/invite|dsc\.gg)[/ ]{,3}[\w-]{1,25}(?!\w))') @@ -62,7 +63,7 @@ async def is_log_enabled(self, guild_id: int, log: str) -> list[int]: res.append(channel) return res - async def validate_logs(self, guild: discord.Guild, channel_ids: list[int], embed: discord.Embed): + async def validate_logs(self, guild: discord.Guild, channel_ids: list[int], embed: discord.Embed, log_type: str): "Send a log embed to the corresponding modlogs channels" for channel_id in channel_ids: if channel := guild.get_channel_or_thread(channel_id): @@ -70,6 +71,7 @@ async def validate_logs(self, guild: discord.Guild, channel_ids: list[int], embe self.to_send[channel].append(embed) else: self.to_send[channel] = [embed] + self.bot.dispatch("serverlog", guild.id, channel.id, log_type) async def db_get_from_channel(self, guild: int, channel: int, use_cache: bool=True) -> list[str]: "Get enabled logs for a channel" @@ -80,7 +82,8 @@ async def db_get_from_channel(self, guild: int, channel: int, use_cache: bool=Tr return [row['kind'] for row in query_results] async def db_get_from_guild(self, guild: int, use_cache: bool=True) -> dict[int, list[str]]: - "Get enabled logs for a guild" + """Get enabled logs for a guild + Returns a map of ChannelID -> list of enabled logs""" if use_cache and (cached := self.cache.get(guild)): return cached query = "SELECT channel, kind FROM serverlogs WHERE guild = %s AND beta = %s" @@ -119,7 +122,7 @@ async def send_logs_task(self): "Send ready logs every 30s to avoid rate limits" try: for channel, embeds in dict(self.to_send).items(): - if not embeds: + if not embeds or channel.guild.me is None: self.to_send.pop(channel) continue try: @@ -140,19 +143,29 @@ async def before_logs_task(self): await self.bot.wait_until_ready() - @commands.group(name="modlogs") + @commands.hybrid_group(name="modlogs") + @app_commands.default_permissions(manage_guild=True) @commands.guild_only() - @commands.check(checks.has_audit_logs) + @commands.check(checks.has_manage_guild) @commands.cooldown(2, 6, commands.BucketType.guild) async def modlogs_main(self, ctx: MyContext): - """Enable or disable server logs in specific channels""" + """Enable or disable server logs in specific channels + + ..Doc moderator.html#server-logs""" if ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx, ['modlogs']) + await ctx.send_help(ctx.command) @modlogs_main.command(name="list") + @app_commands.describe(channel="The channel to list logs for. Leave empty to list all logs for the server") + @commands.guild_only() + @commands.check(checks.has_manage_guild) @commands.cooldown(1, 10, commands.BucketType.channel) - async def modlogs_list(self, ctx: MyContext, channel: discord.TextChannel=None): - """Show the full list of server logs type, or the list of enabled logs for a channel""" + async def modlogs_list(self, ctx: MyContext, channel: Optional[discord.TextChannel]=None): + """Show the full list of server logs type, or the list of enabled logs for a channel + + ..Example modlogs list + + ..Doc moderator.html#how-to-setup-logs""" if channel: # display logs enabled for this channel only title = await self.bot._(ctx.guild.id, "serverlogs.list.channel", channel='#'+channel.name) if channel_logs := await self.db_get_from_channel(ctx.guild.id, channel.id): @@ -187,42 +200,98 @@ async def modlogs_list(self, ctx: MyContext, channel: discord.TextChannel=None): await ctx.send(embed=embed) @modlogs_main.command(name="enable", aliases=['add']) - async def modlogs_enable(self, ctx: MyContext, logs: commands.Greedy[serverlog]): - """Enable one or more logs in the current channel""" - logs: list[str] + @app_commands.describe(channel="The channel to add logs to. Leave empty to select the current channel") + @commands.guild_only() + @commands.check(checks.has_manage_guild) + async def modlogs_enable(self, ctx: MyContext, logs: commands.Greedy[serverlog], channel: Optional[discord.TextChannel]=None): + """Enable one or more logs in the current channel + + ..Example modlogs enable ban bot_warnings + + ..Example modlogs enable role_creation bot_warnings #mod-logs + + ..Doc moderator.html#how-to-setup-logs""" if len(logs) == 0: raise commands.BadArgument('Invalid server log type') + dest_channel = channel or ctx.channel if 'all' in logs: logs = list(self.available_logs()) actually_added: list[str] = [] for log in logs: - if await self.db_add(ctx.guild.id, ctx.channel.id, log): + if await self.db_add(ctx.guild.id, dest_channel.id, log): actually_added.append(log) if actually_added: - msg = await self.bot._(ctx.guild.id, "serverlogs.enabled", kind=', '.join(actually_added)) - if not ctx.channel.permissions_for(ctx.guild.me).embed_links: + if dest_channel == ctx.channel: + msg = await self.bot._(ctx.guild.id, "serverlogs.enabled.current", kind=', '.join(actually_added)) + else: + msg = await self.bot._(ctx.guild.id, "serverlogs.enabled.other", kind=', '.join(actually_added), channel=dest_channel.mention) + if not dest_channel.permissions_for(ctx.guild.me).embed_links: msg += "\n:warning: " + await self.bot._(ctx.guild.id, "serverlogs.embed-warning") else: msg = await self.bot._(ctx.guild.id, "serverlogs.none-added") await ctx.send(msg) + @modlogs_enable.autocomplete("logs") + async def _modlogs_enable_autocomplete(self, interaction: discord.Interaction, current: str): + if channel := interaction.namespace.channel: + channel_id: int = channel.id + else: + channel_id = interaction.channel_id + actived_logs = await self.db_get_from_channel(interaction.guild_id, channel_id) + available_logs = self.available_logs() - set(actived_logs) + return await self.log_name_autocomplete(current, available_logs) + @modlogs_main.command(name="disable", aliases=['remove']) - async def modlogs_disable(self, ctx: MyContext, logs: commands.Greedy[serverlog]): - """Disable one or more logs in the current channel""" - logs: list[str] + @app_commands.describe(channel="The channel to remove logs from. Leave empty to select the current channel") + @commands.guild_only() + @commands.check(checks.has_manage_guild) + async def modlogs_disable(self, ctx: MyContext, logs: commands.Greedy[serverlog], channel: Optional[discord.TextChannel]=None): + """Disable one or more logs in the current channel + + ..Example modlogs disable ban message_delete + + ..Example modlogs disable ghost_ping #mod-logs + + ..Doc moderator.html#how-to-setup-logs""" if len(logs) == 0: raise commands.BadArgument('Invalid server log type') if 'all' in logs: logs = list(self.available_logs()) + dest_channel = channel or ctx.channel actually_removed: list[str] = [] for log in logs: - if await self.db_remove(ctx.guild.id, ctx.channel.id, log): + if await self.db_remove(ctx.guild.id, dest_channel.id, log): actually_removed.append(log) if actually_removed: - msg = await self.bot._(ctx.guild.id, "serverlogs.disabled", kind=', '.join(actually_removed)) + if dest_channel == ctx.channel: + msg = await self.bot._(ctx.guild.id, "serverlogs.disabled.current", kind=', '.join(actually_removed)) + else: + msg = await self.bot._(ctx.guild.id, "serverlogs.disabled.other", kind=', '.join(actually_removed), channel=dest_channel.mention) else: msg = await self.bot._(ctx.guild.id, "serverlogs.none-removed") await ctx.send(msg) + + @modlogs_disable.autocomplete("logs") + async def _modlogs_disable_autocomplete(self, interaction: discord.Interaction, current: str): + if channel := interaction.namespace.channel: + channel_id: int = channel.id + else: + channel_id = interaction.channel_id + actived_logs = await self.db_get_from_channel(interaction.guild_id, channel_id) + return await self.log_name_autocomplete(current, actived_logs) + + async def log_name_autocomplete(self, current: str, available_logs: Optional[list[str]]=None): + "Autocompletion for log names" + all_logs = list(self.available_logs()) if available_logs is None else available_logs + filtered = sorted( + (not option.startswith(current), option) + for option in all_logs + if current in option + ) + return [ + app_commands.Choice(name=value[1], value=value[1]) + for value in filtered + ][:25] @commands.Cog.listener() @@ -265,7 +334,7 @@ async def on_raw_message_edit(self, msg: discord.RawMessageUpdateEvent): if author: emb.set_author(name=str(author), icon_url=author.display_avatar) emb.add_field(name="Message Author", value=f"{author} ({author.id})") - await self.validate_logs(guild, channel_ids, emb) + await self.validate_logs(guild, channel_ids, emb, "message_update") @commands.Cog.listener() async def on_raw_message_delete(self, payload: discord.RawMessageDeleteEvent): @@ -287,7 +356,7 @@ async def on_raw_message_delete(self, payload: discord.RawMessageDeleteEvent): emb.set_author(name=str(msg.author), icon_url=msg.author.display_avatar) emb.add_field(name="Created at", value=f"") emb.add_field(name="Message Author", value=f"{msg.author} ({msg.author.id})") - await self.validate_logs(guild, channel_ids, emb) + await self.validate_logs(guild, channel_ids, emb, "message_delete") # ghost_ping if payload.cached_message is not None and (channel_ids := await self.is_log_enabled(payload.guild_id, "ghost_ping")): msg = payload.cached_message @@ -300,7 +369,7 @@ async def on_raw_message_delete(self, payload: discord.RawMessageDeleteEvent): emb.add_field(name="Created at", value=f"") emb.add_field(name="Message Author", value=f"{msg.author} ({msg.author.id})") emb.add_field(name="Mentionning", value=" ".join(f"<@{mention}>" for mention in set(msg.raw_mentions)), inline=False) - await self.validate_logs(guild, channel_ids, emb) + await self.validate_logs(guild, channel_ids, emb, "ghost_ping") @commands.Cog.listener() async def on_raw_bulk_message_delete(self, payload: discord.RawBulkMessageDeleteEvent): @@ -316,7 +385,7 @@ async def on_raw_bulk_message_delete(self, payload: discord.RawBulkMessageDelete description=f"**{len(payload.message_ids)} messages deleted in <#{payload.channel_id}>**", colour=discord.Color.red() ) - await self.validate_logs(guild, channel_ids, emb) + await self.validate_logs(guild, channel_ids, emb, "message_delete") @commands.Cog.listener() async def on_message(self, message: discord.Message): @@ -337,7 +406,7 @@ async def on_message(self, message: discord.Message): emb.add_field(name="Invite" if len(invites) == 1 else "Invites", value="\n".join(invites_formatted), inline=False) except Exception as err: print(err) - await self.validate_logs(message.guild, channel_ids, emb) + await self.validate_logs(message.guild, channel_ids, emb, "discord_invite") @commands.Cog.listener() async def on_member_update(self, before: discord.Member, after: discord.Member): @@ -393,7 +462,7 @@ async def handle_member_roles(self, before: discord.Member, after: discord.Membe if entry.target.id == before.id and (now - entry.created_at).total_seconds() < 5: emb.add_field(name="Roles edited by", value=f"**{entry.user.mention}** ({entry.user.id})") break - await self.validate_logs(after.guild, channel_ids, emb) + await self.validate_logs(after.guild, channel_ids, emb, "member_roles") async def handle_member_nick(self, before: discord.Member, after: discord.Member, channel_ids: list[int]): "Handle member_nick log" @@ -405,7 +474,7 @@ async def handle_member_nick(self, before: discord.Member, after: discord.Member after_txt = "None" if after.nick is None else discord.utils.escape_markdown(after.nick) emb.add_field(name="Nickname edited", value=f"{before_txt} -> {after_txt}") emb.set_author(name=str(after), icon_url=after.avatar or after.default_avatar) - await self.validate_logs(after.guild, channel_ids, emb) + await self.validate_logs(after.guild, channel_ids, emb, "member_nick") async def handle_member_avatar(self, before: discord.Member, after: discord.Member, channel_ids: list[int]): "Handle member_avatar log" @@ -417,7 +486,7 @@ async def handle_member_avatar(self, before: discord.Member, after: discord.Memb after_txt = "None" if after.guild_avatar is None else f"[After]{after.guild_avatar}" emb.add_field(name="Server avatar edited", value=f"{before_txt} -> {after_txt}") emb.set_author(name=str(after), icon_url=after.avatar or after.default_avatar) - await self.validate_logs(after.guild, channel_ids, emb) + await self.validate_logs(after.guild, channel_ids, emb, "member_avatar") async def handle_member_timeout(self, before: discord.Member, after: discord.Member, channel_ids: list[int]): "Handle member_timeout log at start" @@ -440,8 +509,9 @@ async def handle_member_timeout(self, before: discord.Member, after: discord.Mem ): emb.add_field( name="Timeout by", value=f"**{entry.user.mention}** ({entry.user.id})") + emb.add_field(name="With reason", value=entry.reason or "No reason specified") break - await self.validate_logs(after.guild, channel_ids, emb) + await self.validate_logs(after.guild, channel_ids, emb, "member_timeout") async def handle_member_untimeout(self, before: discord.Member, after: discord.Member, channel_ids: list[int]): "Handle member_timeout log at end" @@ -464,7 +534,7 @@ async def handle_member_untimeout(self, before: discord.Member, after: discord.M emb.add_field( name="Revoked by", value=f"**{entry.user.mention}** ({entry.user.id})") break - await self.validate_logs(after.guild, channel_ids, emb) + await self.validate_logs(after.guild, channel_ids, emb, "member_timeout") async def handle_member_verification(self, _before: discord.Member, after: discord.Member, channel_ids: list[int]): "Handle member_verification log" @@ -485,7 +555,7 @@ async def handle_member_verification(self, _before: discord.Member, after: disco name="Joined at", value=f" ({delta})", inline=False) - await self.validate_logs(after.guild, channel_ids, emb) + await self.validate_logs(after.guild, channel_ids, emb, "member_verification") async def get_member_specs(self, member: discord.Member) -> list[str]: "Get specific things to note for a member" @@ -513,7 +583,7 @@ async def on_member_join(self, member: discord.Member): emb.add_field(name="Account created at", value=f"", inline=False) if specs := await self.get_member_specs(member): emb.add_field(name="Specificities", value=", ".join(specs), inline=False) - await self.validate_logs(member.guild, channel_ids, emb) + await self.validate_logs(member.guild, channel_ids, emb, "member_join") @commands.Cog.listener() async def on_raw_member_remove(self, payload: discord.RawMemberRemoveEvent): @@ -554,14 +624,14 @@ async def handle_member_leave(self, payload: discord.RawMemberRemoveEvent, chann member_roles = [role for role in payload.user.roles[::-1] if not role.is_default()] roles_value = " ".join(r.mention for r in member_roles[:20]) if member_roles else "None" emb.add_field(name=f"Roles ({len(member_roles)})", value=roles_value) - await self.validate_logs(guild, channel_ids, emb) + await self.validate_logs(guild, channel_ids, emb, "member_leave") async def handle_member_kick(self, payload: discord.RawMemberRemoveEvent, channel_ids: list[int]): "Handle member_kick log" guild = self.bot.get_guild(payload.guild_id) if guild is None: return - if not guild.me.guild_permissions.view_audit_log: + if guild.me is None or not guild.me.guild_permissions.view_audit_log: return now = self.bot.utcnow() await asyncio.sleep(self.auditlogs_timeout) @@ -576,7 +646,7 @@ async def handle_member_kick(self, payload: discord.RawMemberRemoveEvent, channe value=f"**{entry.user.mention}** ({entry.user.id})") emb.add_field(name="With reason", value=entry.reason or "No reason specified") - await self.validate_logs(guild, channel_ids, emb) + await self.validate_logs(guild, channel_ids, emb, "member_kick") break @@ -599,7 +669,7 @@ async def on_member_ban(self, guild: discord.Guild, user: discord.User): emb.add_field(name="Banned by", value=f"**{entry.user.mention}** ({entry.user.id})") emb.add_field(name="With reason", value=entry.reason or "No reason specified") break - await self.validate_logs(guild, channel_ids, emb) + await self.validate_logs(guild, channel_ids, emb, "member_ban") @commands.Cog.listener() async def on_member_unban(self, guild: discord.Guild, user: discord.User): @@ -620,7 +690,7 @@ async def on_member_unban(self, guild: discord.Guild, user: discord.User): emb.add_field(name="Unbanned by", value=f"**{entry.user.mention}** ({entry.user.id})") emb.add_field(name="With reason", value=entry.reason or "No reason specified") break - await self.validate_logs(guild, channel_ids, emb) + await self.validate_logs(guild, channel_ids, emb, "member_unban") @commands.Cog.listener() @@ -647,7 +717,7 @@ async def on_guild_role_create(self, role: discord.Role): specs.append("Hoisted") if specs: emb.add_field(name="Specificities", value=", ".join(specs), inline=False) - await self.validate_logs(role.guild, channel_ids, emb) + await self.validate_logs(role.guild, channel_ids, emb, "role_creation") @commands.Cog.listener() async def on_antiscam_warn(self, message: discord.Message, prediction: PredictionResult): @@ -659,7 +729,7 @@ async def on_antiscam_warn(self, message: discord.Message, prediction: Predictio colour=discord.Color.orange() ) await self.prepare_antiscam_embed(message, prediction, emb) - await self.validate_logs(message.guild, channel_ids, emb) + await self.validate_logs(message.guild, channel_ids, emb, "antiscam") @commands.Cog.listener() async def on_antiscam_delete(self, message: discord.Message, prediction: PredictionResult): @@ -671,7 +741,7 @@ async def on_antiscam_delete(self, message: discord.Message, prediction: Predict colour=discord.Color.red() ) await self.prepare_antiscam_embed(message, prediction, emb) - await self.validate_logs(message.guild, channel_ids, emb) + await self.validate_logs(message.guild, channel_ids, emb, "antiscam") async def prepare_antiscam_embed(self, message: discord.Message, prediction: PredictionResult, emb: discord.Embed): "Prepare the embed for an antiscam alert" @@ -704,7 +774,7 @@ async def on_antiraid_kick(self, member: discord.Member, data: dict[str, Any]): emb.add_field(name="Account was too recent", value=value, inline=False) if "discord_invite" in data: emb.add_field(name="Contains a Discord invite in their username", value=self.bot.zws, inline=False) - await self.validate_logs(member.guild, channel_ids, emb) + await self.validate_logs(member.guild, channel_ids, emb, "antiraid") @commands.Cog.listener() async def on_antiraid_ban(self, member: discord.Member, data: dict[str, Any]): @@ -730,7 +800,7 @@ async def on_antiraid_ban(self, member: discord.Member, data: dict[str, Any]): # duration duration = await FormatUtils.time_delta(data["duration"], hour=(data["duration"] < 86400)) emb.add_field(name="Duration", value=duration) - await self.validate_logs(member.guild, channel_ids, emb) + await self.validate_logs(member.guild, channel_ids, emb, "antiraid") @commands.Cog.listener() async def on_ticket_creation(self, event: TicketCreationEvent): @@ -744,7 +814,7 @@ async def on_ticket_creation(self, event: TicketCreationEvent): emb.add_field(name="Topic", value=event.topic_name) emb.add_field(name="Ticket name", value=event.name) emb.add_field(name="Channel", value=event.channel.mention, inline=False) - await self.validate_logs(event.guild, channel_ids, emb) + await self.validate_logs(event.guild, channel_ids, emb, "ticket_creation") @commands.Cog.listener() async def on_server_warning(self, warning_type: ServerWarningType, guild: discord.Guild, **kwargs): @@ -785,7 +855,7 @@ async def on_server_warning(self, warning_type: ServerWarningType, guild: discor emb.add_field(name="Reason", value="Too many recent errors") else: return - await self.validate_logs(guild, channel_ids, emb) + await self.validate_logs(guild, channel_ids, emb, "bot_warnings") async def setup(bot): diff --git a/fcts/servers.py b/fcts/servers.py index 7fc92ca0..df23366e 100644 --- a/fcts/servers.py +++ b/fcts/servers.py @@ -6,31 +6,17 @@ import discord import emoji from cachingutils import LRUCache +from discord import app_commands from discord.ext import commands + from libs.bot_classes import MyContext, Zbot +from libs.serverconfig import options_list as opt_list +from libs.serverconfig.autocomplete import autocomplete_main +from libs.serverconfig.config_paginator import ServerConfigPaginator +from libs.views import ConfirmView from . import checks -roles_options = ["clear_allowed_roles", "slowmode_allowed_roles", "mute_allowed_roles", "kick_allowed_roles", "ban_allowed_roles", - "warn_allowed_roles", "say_allowed_roles", "welcome_roles", "muted_role", 'partner_role', 'update_mentions', - "voice_roles"] -bool_options = ["enable_xp", "anti_caps_lock", "enable_fun", - "help_in_dm", "compress_help", "anti_scam", "nicknames_history"] -textchan_options = ["welcome_channel", "bot_news", "poll_channels", - "modlogs_channel", "noxp_channels", "partner_channel"] -vocchan_options = ["membercounter", "voice_channel"] -category_options = ["voice_category"] -text_options = ["welcome", "leave", "levelup_msg", - "description", "voice_channel_format"] -prefix_options = ['prefix'] -emoji_option = ['vote_emojis', 'morpion_emojis'] -numb_options = [] -raid_options = ["anti_raid"] -xp_type_options = ['xp_type'] -color_options = ['partner_color'] -xp_rate_option = ['xp_rate'] -levelup_channel_option = ["levelup_channel"] -ttt_display_option = ["ttt_display"] class Servers(commands.Cog): """"Cog in charge of all the bot configuration management for your server. As soon as an option @@ -43,69 +29,28 @@ def __init__(self, bot: Zbot): self.log_color = 1793969 self.file = "servers" self.cache: LRUCache = LRUCache(max_size=10000, timeout=3600) - self.raids_levels = ["None","Smooth","Careful","High","(╯°□°)╯︵ ┻━┻"] - self.default_opt = {"rr_max_number":7, - "rss_max_number":10, - "roles_react_max_number":20, - "language":1, - "description":"", - "clear_allowed_roles":"", - "slowmode_allowed_roles":"", - "mute_allowed_roles":"", - "kick_allowed_roles":"", - "ban_allowed_roles":"", - "warn_allowed_roles":"", - "say_allowed_roles":"", - "hunter":"", - "welcome_channel":'', - "welcome":"", - "leave":"", - "welcome_roles":"", - "bot_news":'', - "save_roles":0, - "poll_channels":"", - "modlogs_channel":"", - "enable_xp":0, - "levelup_msg":'', - "levelup_channel":'any', - "noxp_channels":'', - "xp_rate":1.0, - "xp_type":0, - "anti_caps_lock":0, - "enable_fun":1, - "prefix":'!', - "membercounter":"", - "anti_raid":0, - "vote_emojis":":thumbsup:;:thumbsdown:;", - "morpion_emojis":":red_circle:;:blue_circle:;", - "help_in_dm":0, - "muted_role":"", - "partner_channel":'', - "partner_color":10949630, - 'partner_role':'', - 'update_mentions':'', - 'voice_roles':'', - 'voice_channel':'', - 'voice_category':'', - 'voice_channel_format': '{random}', - 'compress_help': 0, - 'ttt_display': 2, - 'anti_scam': 0, - 'nicknames_history': None, - } - self.optionsList = ["prefix","language","description","clear_allowed_roles","slowmode_allowed_roles","mute_allowed_roles","kick_allowed_roles","ban_allowed_roles","warn_allowed_roles","say_allowed_roles","welcome_channel","welcome","leave","welcome_roles","anti_scam","poll_channels","partner_channel","partner_color","partner_role","modlogs_channel","nicknames_history","enable_xp","levelup_msg","levelup_channel","noxp_channels","xp_rate","xp_type","anti_caps_lock","enable_fun","membercounter","anti_raid","vote_emojis","morpion_emojis","help_in_dm","compress_help","muted_role","voice_roles","voice_channel","voice_category","voice_channel_format","ttt_display","bot_news","update_mentions"] - self.membercounter_pending = {} + self.raids_levels = ["None", "Smooth", "Careful", "High", "(╯°□°)╯︵ ┻━┻"] + self.options_list = [ + "prefix", "language", "description", "clear_allowed_roles", "slowmode_allowed_roles", "mute_allowed_roles", + "kick_allowed_roles", "ban_allowed_roles", "warn_allowed_roles", "say_allowed_roles", "welcome_channel", + "welcome", "leave", "welcome_roles", "anti_scam", "poll_channels", "partner_channel", "partner_color", + "partner_role", "modlogs_channel", "nicknames_history", "enable_xp", "levelup_msg", "levelup_channel", + "noxp_channels", "xp_rate", "xp_type", "anti_caps_lock", "enable_fun", "membercounter", "anti_raid", + "vote_emojis", "morpion_emojis", "help_in_dm", "compress_help", "muted_role", "voice_roles", "voice_channel", + "voice_category", "voice_channel_format", "ttt_display", "bot_news", "update_mentions", "streaming_channel", + "stream_mention", "streaming_role", + ] + self.membercounter_pending: dict[int, int] = {} self.max_members_for_nicknames = 3000 - @property - def table(self): - return 'servers' + async def clear_cache(self): + self.cache._items.clear() - async def get_bot_infos(self, botID: int): + async def get_bot_infos(self, bot_id: int): """Return every options of the bot""" if not self.bot.database_online: return list() - query = ("SELECT * FROM `bot_infos` WHERE `ID`={}".format(botID)) + query = ("SELECT * FROM `bot_infos` WHERE `ID`={}".format(bot_id)) async with self.bot.db_query(query) as query_results: liste = list(query_results) return liste @@ -123,10 +68,10 @@ async def get_languages(self, ignored_guilds: typing.List[int], return_dict: boo """Return stats on used languages""" if not self.bot.database_online or not 'Languages' in self.bot.cogs: return [] - query = f"SELECT `language`,`ID` FROM `{self.table}`" + query = "SELECT `language`,`ID` FROM `servers` WHERE `beta` = %s" liste = [] guilds = {x.id for x in self.bot.guilds if x.id not in ignored_guilds} - async with self.bot.db_query(query) as query_results: + async with self.bot.db_query(query, (self.bot.beta,)) as query_results: for row in query_results: if row['ID'] in guilds: liste.append(row['language']) @@ -143,31 +88,31 @@ async def get_languages(self, ignored_guilds: typing.List[int], return_dict: boo return langs async def get_xp_types(self, ignored_guilds: typing.List[int], return_dict: bool = False): - """Return stats on used xp types""" + "Return stats on used xp types" if not self.bot.database_online: - return list() - query = ("SELECT `xp_type`,`ID` FROM `{}`".format(self.table)) - liste = list() + return [] + query = "SELECT `xp_type`,`ID` FROM `servers` WHERE `beta` = %s" + liste = [] guilds = {x.id for x in self.bot.guilds if x.id not in ignored_guilds} - async with self.bot.db_query(query) as query_results: + async with self.bot.db_query(query, (self.bot.beta,)) as query_results: for row in query_results: if row['ID'] in guilds: liste.append(row['xp_type']) for _ in range(len(guilds)-len(liste)): - liste.append(self.default_opt['xp_type']) + liste.append(opt_list.default_values['xp_type']) if return_dict: - types = dict() + types = {} for e, name in enumerate(self.bot.get_cog('Xp').types): types[name] = liste.count(e) else: - types = list() + types = [] for e, name in enumerate(self.bot.get_cog('Xp').types): types.append((name, liste.count(e))) return types async def staff_finder(self, user: discord.Member, option: str): """Check is user is part of a staff""" - if option not in roles_options: + if option not in opt_list.roles_options: raise TypeError if await self.bot.get_cog('Admin').check_if_god(user): return True @@ -182,28 +127,28 @@ async def staff_finder(self, user: discord.Member, option: str): return True raise commands.CommandError("User doesn't have required roles") - async def get_option(self, guild_id: int, name: str) -> typing.Optional[str]: + async def get_option(self, guild_id: typing.Union[discord.Guild, int], option_name: str) -> typing.Optional[str]: """return the value of an option - Return None if this option doesn't exist or if no value has been set""" + Return None if this option doesn't exist or if no value has been set (like if the guild isn't in the database)""" if isinstance(guild_id, discord.Guild): guild_id = guild_id.id elif guild_id is None or not self.bot.database_online: return None - if (cached := self.cache.get((guild_id, name))) is not None: + if (cached := self.cache.get((guild_id, option_name))) is not None: return cached - sql_result = await self.get_server([name],criters=["ID="+str(guild_id)],return_type=list) + sql_result = await self.get_server([option_name],criters=["ID="+str(guild_id)],return_type=list) if len(sql_result) == 0: value = None elif sql_result[0][0] == '': - if name == "nicknames_history": + if option_name == "nicknames_history": value = None else: - value = self.default_opt[name] + value = opt_list.default_values[option_name] else: value = sql_result[0][0] - if value is None and name == "nicknames_history" and (guild := self.bot.get_guild(guild_id)): + if value is None and option_name == "nicknames_history" and (guild := self.bot.get_guild(guild_id)): value = len(guild.members) < self.max_members_for_nicknames - self.cache[(guild_id, name)] = value + self.cache[(guild_id, option_name)] = value return value async def get_server(self, columns=[], criters=["ID > 1"], relation="AND", return_type=dict): @@ -216,24 +161,26 @@ async def get_server(self, columns=[], criters=["ID > 1"], relation="AND", retur else: cl = "`"+"`,`".join(columns)+"`" relation = " "+relation+" " - query = ("SELECT {} FROM `{}` WHERE {}".format(cl, self.table, relation.join(criters))) + query = ("SELECT {} FROM `servers` WHERE `beta` = %s AND {}".format(cl, relation.join(criters))) liste = list() - async with self.bot.db_query(query, astuple=(return_type!=dict)) as query_results: + async with self.bot.db_query(query, (self.bot.beta,), astuple=(return_type!=dict)) as query_results: for row in query_results: if isinstance(row, dict): for k, v in row.items(): if v == '': - row[k] = self.default_opt.get(k, None) + row[k] = opt_list.default_values.get(k, None) liste.append(row) return liste - async def modify_server(self, guild_id: int, values=[()]): + async def modify_server(self, guild_id: int, values=None): """Update a server config in the database""" - if not isinstance(values, list): + if values is not None and not isinstance(values, list): raise ValueError + if values is None: + values = [()] set_query = ', '.join(f'`{val[0]}`=%s' for val in values) - query = f"UPDATE `{self.table}` SET {set_query} WHERE `ID`={guild_id}" - async with self.bot.db_query(query, (val[1] for val in values)): + query = f"UPDATE `servers` SET {set_query} WHERE `ID`=%s AND `beta` = %s" + async with self.bot.db_query(query, tuple(val[1] for val in values) + (guild_id, self.bot.beta)): pass for value in values: self.cache[(guild_id, value[0])] = value[1] @@ -241,9 +188,9 @@ async def modify_server(self, guild_id: int, values=[()]): async def delete_option(self, guild_id: int, opt): """Reset an option""" - if opt not in self.default_opt.keys(): + if opt not in opt_list.default_values: raise ValueError - value = self.default_opt[opt] + value = opt_list.default_values[opt] if opt == 'language': await self.bot.get_cog('Languages').change_cache(guild_id,value) elif opt == 'prefix': @@ -255,12 +202,12 @@ async def add_server(self, guild_id: int): if isinstance(guild_id, str): if not guild_id.isnumeric(): raise ValueError - query = "INSERT INTO `{}` (`ID`) VALUES ('{}')".format(self.table,guild_id) - async with self.bot.db_query(query): + query = "INSERT INTO `servers` (`ID`, `beta`) VALUES (%s, %s)" + async with self.bot.db_query(query, (guild_id, self.bot.beta)): pass return True - async def is_server_exist(self, guild_id: int): + async def create_row_if_needed(self, guild_id: int): """Check if a server is already in the db""" i = await self.get_option(guild_id, "ID") if i is None: @@ -277,20 +224,31 @@ async def delete_server(self, guild_id: int): """remove a server from the db""" if not isinstance(guild_id, int): raise ValueError - query = f"DELETE FROM `{self.table}` WHERE `ID`='{guild_id}'" - async with self.bot.db_query(query): + query = f"DELETE FROM `servers` WHERE `ID` = %s AND `beta` = %s" + async with self.bot.db_query(query, (guild_id, self.bot.beta)): pass return True - @commands.group(name='config') + async def option_name_autocomplete(self, current: str): + "Autocompletion for an option name" + blacklisted_autocomplete = {'bot_news', 'update_mentions'} + filtered = sorted( + (not option.startswith(current), option) for option in self.options_list + if option not in blacklisted_autocomplete and current in option + ) + return [ + app_commands.Choice(name=value[1], value=value[1]) + for value in filtered + ][:25] + + @commands.hybrid_group(name='config') + @discord.app_commands.default_permissions(manage_guild=True) @commands.guild_only() async def sconfig_main(self, ctx: MyContext): """Function for setting the bot on a server ..Doc server.html#config-options""" - if ctx.bot.database_online: - await self.is_server_exist(ctx.guild.id) if ctx.invoked_subcommand is None: msg = copy.copy(ctx.message) subcommand_passed = ctx.message.content.replace(ctx.prefix+"config ","") @@ -298,7 +256,7 @@ async def sconfig_main(self, ctx: MyContext): msg.content = ctx.prefix + "config help" elif subcommand_passed.isnumeric(): msg.content = ctx.prefix + "config see " + subcommand_passed - elif subcommand_passed.split(" ")[0] in self.optionsList: + elif subcommand_passed.split(" ")[0] in self.options_list: if len(subcommand_passed.split(" "))==1: msg.content = ctx.prefix + "config see " + subcommand_passed else: @@ -308,15 +266,10 @@ async def sconfig_main(self, ctx: MyContext): new_ctx = await self.bot.get_context(msg) await self.bot.invoke(new_ctx) - @sconfig_main.command(name="help") - @commands.cooldown(1, 2, commands.BucketType.guild) - async def sconfig_help(self, ctx: MyContext): - """Get help about this command""" - msg = await self.bot._(ctx.guild, "server.config-help", p=await self.bot.prefix_manager.get_prefix(ctx.guild)) - await ctx.send(msg.format(ctx.guild.owner.name)) - - @sconfig_main.command(name="reset", aliases=["delete", "del"]) + @sconfig_main.command(name="reset") + @app_commands.describe(option="The option to reset") @commands.cooldown(1, 2, commands.BucketType.guild) + @commands.guild_only() @commands.check(checks.has_manage_guild) async def sconfig_del(self, ctx: MyContext, option: str): """Reset an option to its initial value""" @@ -324,65 +277,108 @@ async def sconfig_del(self, ctx: MyContext, option: str): return await ctx.send(await self.bot._(ctx.guild.id,"cases.no_database")) await self.sconfig_del2(ctx, option) + @sconfig_del.autocomplete("option") + async def sconfig_del_autocomplete(self, _: discord.Interaction, option: str): + return await self.option_name_autocomplete(option) + @sconfig_main.command(name="change") + @app_commands.describe( + option="The option to modify", + value="The new option value" + ) @commands.cooldown(1, 2, commands.BucketType.guild) + @commands.guild_only() @commands.check(checks.has_manage_guild) async def sconfig_change(self, ctx: MyContext, option:str, *, value: str): """Allows you to modify an option""" if not ctx.bot.database_online: return await ctx.send(await self.bot._(ctx.guild.id,"cases.no_database")) - if value == 'del': - await self.sconfig_del2(ctx, option) + await self.create_row_if_needed(ctx.guild.id) + if option in opt_list.roles_options: + await self.conf_roles(ctx, option, value) + elif option in opt_list.bool_options: + await self.conf_bool(ctx, option, value) + elif option in opt_list.textchannels_options: + await self.conf_textchan(ctx, option, value) + elif option in opt_list.category_options: + await self.conf_category(ctx, option, value) + elif option in opt_list.text_options: + await self.conf_text(ctx, option, value) + elif option in opt_list.numb_options: + await self.conf_numb(ctx, option, value) + elif option in opt_list.voicechannels_options: + await self.conf_vocal(ctx, option, value) + elif option == "language": + await self.conf_lang(ctx, option, value) + elif option in opt_list.prefix_options: + await self.conf_prefix(ctx, option, value) + elif option in opt_list.raid_options: + await self.conf_raid(ctx, option, value) + elif option in opt_list.emoji_option: + await self.conf_emoji(ctx, option, value) + elif option in opt_list.xp_type_options: + await self.conf_xp_type(ctx, option, value) + elif option in opt_list.color_options: + await self.conf_color(ctx, option, value) + elif option in opt_list.xp_rate_option: + await self.conf_xp_rate(ctx, option, value) + elif option in opt_list.levelup_channel_option: + await self.conf_levelup_chan(ctx, option, value) + elif option in opt_list.ttt_display_option: + await self.conf_tttdisplay(ctx, option, value) + else: + await ctx.send(await self.bot._(ctx.guild.id, "server.option-notfound")) return - try: - if option in roles_options: - await self.conf_roles(ctx, option, value) - elif option in bool_options: - await self.conf_bool(ctx, option, value) - elif option in textchan_options: - await self.conf_textchan(ctx, option, value) - elif option in category_options: - await self.conf_category(ctx, option, value) - elif option in text_options: - await self.conf_text(ctx, option, value) - elif option in numb_options: - await self.conf_numb(ctx, option, value) - elif option in vocchan_options: - await self.conf_vocal(ctx, option, value) - elif option == "language": - await self.conf_lang(ctx, option, value) - elif option in prefix_options: - await self.conf_prefix(ctx, option, value) - elif option in raid_options: - await self.conf_raid(ctx, option, value) - elif option in emoji_option: - await self.conf_emoji(ctx, option, value) - elif option in xp_type_options: - await self.conf_xp_type(ctx, option, value) - elif option in color_options: - await self.conf_color(ctx, option, value) - elif option in xp_rate_option: - await self.conf_xp_rate(ctx, option, value) - elif option in levelup_channel_option: - await self.conf_levelup_chan(ctx, option, value) - elif option in ttt_display_option: - await self.conf_tttdisplay(ctx, option, value) - else: - await ctx.send(await self.bot._(ctx.guild.id, "server.option-notfound")) - return - except Exception as e: - await self.bot.get_cog("Errors").on_error(e,ctx) - await ctx.send(await self.bot._(ctx.guild.id, "server.internal-error")) + + @sconfig_change.autocomplete("option") + async def sconfig_change_autocomplete_opt(self, _: discord.Interaction, option: str): + return await self.option_name_autocomplete(option) + + @sconfig_change.autocomplete("value") + async def sconfig_change_autocomplete_value(self, interaction: discord.Interaction, value: str): + return await autocomplete_main(self.bot, interaction, interaction.namespace.option, value) + + @sconfig_main.command(name="reset-all") + @commands.guild_only() + @commands.check(checks.has_admin) + async def admin_delete(self, ctx: MyContext): + """Reset the whole config of your server + VERY DANGEROUS, NO ROLLBACK POSSIBLE""" + text = await self.bot._(ctx.guild.id, "server.reset-all.confirmation") + confirm_view = ConfirmView( + self.bot, ctx.channel, + validation=lambda inter: inter.user == ctx.author, + ephemeral=False, + send_confirmation=False + ) + await confirm_view.init() + confirm_msg = await ctx.send(text, view=confirm_view) + await confirm_view.wait() + await confirm_view.disable(confirm_msg) + if not confirm_view.value: + return + if await self.delete_server(ctx.guild.id): + await ctx.send(await self.bot._(ctx.guild.id, "server.reset-all.success")) + # Send internal log + msg = f"Reset all options in server {ctx.guild.id}" + emb = discord.Embed(description=msg, color=self.log_color, timestamp=self.bot.utcnow()) + emb.set_footer(text=ctx.guild.name) + emb.set_author(name=self.bot.user, icon_url=self.bot.user.display_avatar) + await self.bot.send_embed(emb) + self.bot.log.info(msg) + else: + await ctx.send(await self.bot._(ctx.guild.id, "server.reset-all.error")) async def sconfig_del2(self, ctx: MyContext, option: str): + "Reset an option for a given guild" try: - t = await self.delete_option(ctx.guild.id,option) - if t: + if await self.delete_option(ctx.guild.id,option): msg = await self.bot._(ctx.guild.id, "server.value-deleted", option=option) else: - msg = await self.bot._(ctx.guild.id, "server.internal-error") + await ctx.send(await self.bot._(ctx.guild.id, "server.internal-error")) + return await ctx.send(msg) - msg = "Reset option in server {}: {}".format(ctx.guild.id, option) + msg = f"Reset option in server {ctx.guild.id}: {option}" emb = discord.Embed(description=msg, color=self.log_color, timestamp=self.bot.utcnow()) emb.set_footer(text=ctx.guild.name) emb.set_author(name=self.bot.user, icon_url=self.bot.user.display_avatar) @@ -391,11 +387,11 @@ async def sconfig_del2(self, ctx: MyContext, option: str): except ValueError: await ctx.send(await self.bot._(ctx.guild.id, "server.option-notfound")) except Exception as err: - await self.bot.get_cog("Errors").on_error(err,ctx) - await ctx.send(await self.bot._(ctx.guild.id, "server.internal-error")) + self.bot.dispatch("command_error", ctx, err) async def send_embed(self, guild: discord.Guild, option: str, value: str): - msg = "Changed option in server {}: {} = `{}`".format(guild.id,option,value) + "Send a log embed into the private logs channel when an option is edited" + msg = f"Changed option in server {guild.id}: {option} = `{value}`" emb = discord.Embed(description=msg, color=self.log_color, timestamp=self.bot.utcnow()) emb.set_footer(text=guild.name) emb.set_author(name=self.bot.user, icon_url=self.bot.user.display_avatar) @@ -487,14 +483,14 @@ async def conf_bool(self, ctx: MyContext, option: str, value: str): msg = await self.bot._(ctx.guild.id, "server.edit-success.boolean", opt=option, val=value) await ctx.send(msg) await self.send_embed(ctx.guild, option, value) - + async def form_bool(self, boolean): if boolean == 1: v = True else: v = False return v - + async def conf_textchan(self, ctx: MyContext, option: str, value: str): guild = await self.get_guild(ctx) ext = not isinstance(ctx, commands.Context) @@ -532,7 +528,7 @@ async def form_textchan(self, guild: discord.Guild, chans: str, ext=False): chans = chans.split(";") g_chans = list() for r in chans: - g_chan = guild.get_channel(int(r)) + g_chan = guild.get_channel_or_thread(int(r)) if g_chan is None: g_chans.append('<' + await self.bot._(guild, "server.deleted-channel") + '>') elif ext: @@ -569,8 +565,8 @@ async def conf_category(self, ctx: MyContext, option: str, value: str): msg = await self.bot._(guild.id, "server.edit-success.category", opt=option, val=", ".join(liste2)) await ctx.send(msg) await self.send_embed(guild, option, value) - - async def form_category(self, guild: discord.Guild, chans: str, ext=False): + + async def form_category(self, guild: discord.Guild, chans: str, _=False): if len(chans) == 0: return "Ø" chans = chans.split(";") @@ -617,7 +613,7 @@ async def conf_emoji(self, ctx: MyContext, option: str, value: str): async def form_emoji(self, emojis: str, option: str): if len(emojis) == 0: - emojis = self.default_opt[option] + emojis = opt_list.default_values[option] emojis = emojis.split(";") l_em = list() for r in emojis: @@ -757,12 +753,12 @@ async def form_lang(self, value: str): return self.default_language else: return self.bot.get_cog("Languages").languages[value] - + async def conf_raid(self, ctx: MyContext, option: str, value: str): if value == "scret-desc": guild = await self.get_guild(ctx) if guild is None: - return self.default_opt['anti_raid'] + return opt_list.default_values['anti_raid'] v = await self.get_option(guild,option) return await self.form_raid(v) else: @@ -784,10 +780,10 @@ async def conf_raid(self, ctx: MyContext, option: str, value: str): async def form_raid(self, value: str): if value is None: - return self.default_opt['anti_raid'] + return opt_list.default_values['anti_raid'] else: return self.raids_levels[value] - + async def conf_xp_type(self, ctx: MyContext, option: str, value: str): if value == "scret-desc": guild = await self.get_guild(ctx) @@ -812,18 +808,18 @@ async def form_xp_type(self, value: str): return self.bot.get_cog('Xp').types[0] else: return self.bot.get_cog("Xp").types[value] - + async def conf_color(self, ctx: MyContext, option: str, value: str): if value == "scret-desc": guild = await self.get_guild(ctx) if guild is None: - return str(discord.Colour(self.default_opt[option])) + return str(discord.Colour(opt_list.default_values[option])) v = await self.get_option(guild,option) return await self.form_color(option,v) else: try: if value=="default": - color = discord.Color(self.default_opt[option]) + color = discord.Color(opt_list.default_values[option]) else: color = await commands.ColourConverter().convert(ctx,value) except commands.errors.BadArgument: @@ -840,7 +836,7 @@ async def conf_color(self, ctx: MyContext, option: str, value: str): async def form_color(self, option: str, value: str): if value is None: - return str(discord.Colour(self.default_opt[option])) + return str(discord.Colour(opt_list.default_values[option])) else: return str(discord.Colour(value)) @@ -864,7 +860,7 @@ async def conf_xp_rate(self, ctx: MyContext, option: str, value: str): async def form_xp_rate(self, option: str, value: str): if value is None: - return self.default_opt[option] + return opt_list.default_values[option] else: return value @@ -873,6 +869,8 @@ async def conf_levelup_chan(self, ctx: MyContext, option: str, value: str): ext = not isinstance(ctx, commands.Context) if value == "scret-desc": chan = await self.get_option(guild.id,option) + if chan is None: + chan = opt_list.default_values[option] return await self.form_levelup_chan(guild, chan, ext) else: if value.lower() in {"any", "tout", "tous", "current", "all", "any channel"}: @@ -901,7 +899,7 @@ async def form_levelup_chan(self, guild: discord.Guild, value: str, ext: bool=Fa if value == "none": return "Nowhere" if value.isnumeric(): - g_chan = guild.get_channel(int(value)) + g_chan = guild.get_channel_or_thread(int(value)) if g_chan is None: return '<' + await self.bot._(guild, "server.deleted-channel") + '>' elif ext: @@ -909,7 +907,7 @@ async def form_levelup_chan(self, guild: discord.Guild, value: str, ext: bool=Fa else: return g_chan.mention return "" - + async def conf_tttdisplay(self, ctx: MyContext, option: str, value: int): if value == "scret-desc": guild = await self.get_guild(ctx) @@ -918,7 +916,7 @@ async def conf_tttdisplay(self, ctx: MyContext, option: str, value: int): v = await self.get_option(guild, option) return await self.form_tttdisplay(v) else: - available_types: list = self.bot.get_cog("Morpions").types + available_types: list[str] = self.bot.get_cog("Morpions").types value = value.lower() if value in available_types: v = available_types.index(value) @@ -935,22 +933,30 @@ async def form_tttdisplay(self, value: int): return self.bot.get_cog('Morpions').types[0].capitalize() else: return self.bot.get_cog("Morpions").types[value].capitalize() - + @sconfig_main.command(name='list') async def sconfig_list(self, ctx: MyContext): """Get the list of every usable option""" - options = sorted(self.optionsList) - await ctx.send(await self.bot._(ctx.guild.id, "server.config-list",text="\n```\n-{}\n```\n".format('\n-'.join(options)), link="")) + options = sorted(self.options_list) + txt = "\n```\n-{}\n```\n".format('\n-'.join(options)) + link = "" + await ctx.send(await self.bot._(ctx.guild.id, "server.config-list", + text=txt, link=link)) @sconfig_main.command(name="see") @commands.cooldown(1,10,commands.BucketType.guild) - async def sconfig_see(self, ctx: MyContext, option=None): + @commands.guild_only() + async def sconfig_see(self, ctx: MyContext, option: typing.Optional[str]=None): """Displays the value of an option, or all options if none is specified""" if not ctx.bot.database_online: return await ctx.send(await self.bot._(ctx.guild.id,"cases.no_database")) - await self.send_see(ctx.guild,ctx.channel,option,ctx.message,ctx) + await self.send_see(ctx.guild, ctx, option, ctx.message) + + @sconfig_see.autocomplete("option") + async def sconfig_see_autocomplete(self, _: discord.Interaction, option: str): + return await self.option_name_autocomplete(option) - async def send_see(self, guild: discord.Guild, channel: typing.Union[discord.TextChannel, discord.Thread], option: str, msg: discord.Message, ctx: MyContext): + async def send_see(self, guild: discord.Guild, ctx: MyContext, option: str, msg: discord.Message): """Envoie l'embed dans un salon""" if self.bot.zombie_mode: return @@ -958,143 +964,70 @@ async def send_see(self, guild: discord.Guild, channel: typing.Union[discord.Tex option = "1" if option.isnumeric(): page = int(option) - if page<1: - return await ctx.send(await self.bot._(channel, "xp.low-page")) - liste = await self.get_server([],criters=["ID="+str(guild.id)]) - if len(liste) == 0: - return await channel.send(await self.bot._(channel, "server.not-found", guild=guild.name)) - temp = [(k,v) for k,v in liste[0].items() if k in self.optionsList] - max_page = ceil(len(temp)/20) - if page > max_page: - return await ctx.send(await self.bot._(channel, "xp.high-page")) - liste = {k:v for k,v in temp[(page-1)*20:page*20] } - if len(liste) == 0: - return await ctx.send("NOPE") - title = await self.bot._(channel, "server.see-title", guild=guild.name) + f" ({page}/{max_page})" - embed = discord.Embed(title=title, color=self.embed_color, - description=await self.bot._(channel, "server.see-0"), timestamp=msg.created_at) - if guild.icon: - embed.set_thumbnail(url=guild.icon.with_static_format('png')) - diff = channel.guild != guild - for i,v in liste.items(): - #if i not in self.optionsList: - # continue - if i == "nicknames_history" and v is None: - r = len(guild.members) < self.max_members_for_nicknames - elif i in roles_options: - r = await self.form_roles(guild,v,diff) - r = ", ".join(r) - elif i in bool_options: - r = str(await self.form_bool(v)) - elif i in textchan_options: - r = await self.form_textchan(guild,v,diff) - r = ", ".join(r) - elif i in category_options: - r = await self.form_category(guild, v, diff) - r = ', '.join(r) - elif i in text_options: - r = v if len(v)<500 else v[:500]+"..." - elif i in numb_options: - r = str(v) - elif i in vocchan_options: - r = await self.form_vocal(guild,v) - r = ", ".join(r) - elif i == "language": - r = await self.form_lang(v) - elif i in prefix_options: - r = await self.form_prefix(v) - elif i in raid_options: - r = await self.form_raid(v) - elif i in emoji_option: - r = ", ".join(await self.form_emoji(v, i)) - elif i in xp_type_options: - r = await self.form_xp_type(v) - elif i in color_options: - r = await self.form_color(i,v) - elif i in xp_rate_option: - r = await self.form_xp_rate(i,v) - elif i in levelup_channel_option: - r = await self.form_levelup_chan(guild,v,diff) - elif i in ttt_display_option: - r = await self.form_tttdisplay(v) - else: - continue - if len(str(r)) == 0: - r = "Ø" - embed.add_field(name=i, value=r) - await channel.send(embed=embed) + if page < 1: + return await ctx.send(await self.bot._(ctx.channel, "xp.low-page")) + _quit = await self.bot._(ctx.guild, "misc.quit") + view = ServerConfigPaginator(self.bot, ctx.author, stop_label=_quit.capitalize(), guild=guild, cog=self) + await view.send_init(ctx) return elif ctx is not None: - if option in roles_options: + if option in opt_list.roles_options: r = await self.conf_roles(ctx, option, 'scret-desc') r = ", ".join(r) - elif option in bool_options: + elif option in opt_list.bool_options: r = str(await self.conf_bool(ctx, option, 'scret-desc')) - elif option in textchan_options: + elif option in opt_list.textchannels_options: r = await self.conf_textchan(ctx, option, 'scret-desc') r = ", ".join(r) - elif option in category_options: + elif option in opt_list.category_options: r = await self.conf_category(ctx, option, 'scret-desc') r = ', '.join(r) - elif option in text_options: + elif option in opt_list.text_options: r = await self.conf_text(ctx, option, 'scret-desc') - elif option in numb_options: + elif option in opt_list.numb_options: r = await self.conf_numb(ctx, option, 'scret-desc') - elif option in vocchan_options: + elif option in opt_list.voicechannels_options: r = await self.conf_vocal(ctx, option, 'scret-desc') r = ", ".join(r) elif option == "language": r = await self.conf_lang(ctx, option, 'scret-desc') - elif option in prefix_options: + elif option in opt_list.prefix_options: r = await self.conf_prefix(ctx, option, 'scret-desc') - elif option in raid_options: + elif option in opt_list.raid_options: r = await self.conf_raid(ctx, option, 'scret-desc') - elif option in emoji_option: + elif option in opt_list.emoji_option: r = await self.conf_emoji(ctx, option, 'scret-desc') - elif option in xp_type_options: + elif option in opt_list.xp_type_options: r = await self.conf_xp_type(ctx, option, 'scret-desc') - elif option in color_options: + elif option in opt_list.color_options: r = await self.conf_color(ctx, option, 'scret-desc') - elif option in xp_rate_option: + elif option in opt_list.xp_rate_option: r = await self.conf_xp_rate(ctx, option, 'scret-desc') - elif option in levelup_channel_option: + elif option in opt_list.levelup_channel_option: r = await self.conf_levelup_chan(ctx, option, 'scret-desc') - elif option in ttt_display_option: + elif option in opt_list.ttt_display_option: r = await self.conf_tttdisplay(ctx, option, 'scret-desc') else: r = None guild = ctx if isinstance(ctx, discord.Guild) else ctx.guild - if r is not None: + if r is None: + r = await self.bot._(ctx.channel, "server.option-notfound") + else: try: - r = await self.bot._(channel, f"server.server_desc.{option}", value=r) - except Exception as e: + r = await self.bot._(ctx.channel, f"server.server_desc.{option}", value=r) + except Exception as err: pass - else: - r = await self.bot._(channel, "server.option-notfound") try: - if not channel.permissions_for(channel.guild.me).embed_links: - await channel.send(await self.bot._(channel, "minecraft.cant-embed")) + if not ctx.channel.permissions_for(ctx.guild.me).embed_links: + await ctx.send(await self.bot._(ctx.channel, "minecraft.cant-embed")) return - title = await self.bot._(channel, "server.opt_title", opt=option, guild=guild.name) - if hasattr(ctx, "message"): - t = ctx.message.created_at - else: - t = ctx.bot.utcnow() - embed = discord.Embed(title=title, color=self.embed_color, description=r, timestamp=t) + title = await self.bot._(ctx.channel, "server.opt_title", opt=option, guild=guild.name) + embed = discord.Embed(title=title, color=self.embed_color, description=r) if isinstance(ctx, commands.Context): embed.set_footer(text=ctx.author, icon_url=ctx.author.display_avatar) - await channel.send(embed=embed) - except Exception as e: - await self.bot.get_cog('Errors').on_error(e,ctx if isinstance(ctx, commands.Context) else None) - - - @sconfig_main.command(name="reset-guild") - @commands.is_owner() - async def admin_delete(self, ctx: MyContext, ID:int): - "Reset the whole config of a server" - if await self.delete_server(ID): - await ctx.send("Le serveur n°{} semble avoir correctement été supprimé !".format(ID)) - + await ctx.send(embed=embed) + except Exception as err: + self.bot.dispatch("error", err, ctx if isinstance(ctx, commands.Context) else None) async def update_memberChannel(self, guild: discord.Guild): # If we already did an update recently: abort diff --git a/fcts/tickets.py b/fcts/tickets.py index 7bd74899..7a87ecf5 100644 --- a/fcts/tickets.py +++ b/fcts/tickets.py @@ -1,136 +1,23 @@ -import random import time -from typing import Any, Callable, Literal, Optional, Union +from typing import Any, Optional, Union import discord +from discord import app_commands from discord.ext import commands from mysql.connector.errors import IntegrityError + from libs.bot_classes import MyContext, Zbot +from libs.tickets.converters import EmojiConverterType +from libs.tickets.views import (AskTitleModal, AskTopicSelect, SelectView, + SendHintText, TicketCreationEvent) from . import checks -from fcts.args import UnicodeEmoji + def is_named_other(name: str, other_translated: str): "Check if a topic name corresponds to any 'other' variant" return name.lower() in {"other", "others", other_translated} -class TicketCreationEvent: - "Represents a ticket being created" - def __init__(self, topic: dict, name: str, interaction: discord.Interaction, channel: Union[discord.TextChannel, discord.Thread]): - self.topic = topic - self.topic_name = topic["topic"] - self.name = name - self.guild = interaction.guild - self.user = interaction.user - self.channel = channel - -class SelectView(discord.ui.View): - "Used to ask what kind of ticket a user wants to open" - def __init__(self, guild_id: int, topics: list[dict[str, Any]]): - super().__init__(timeout=None) - options = self.build_options(topics) - custom_id = f"{guild_id}-tickets-{random.randint(1, 100):03}" - self.select = discord.ui.Select(placeholder="Chose a topic", options=options, custom_id=custom_id) - self.add_item(self.select) - - def build_options(self, topics: list[dict[str, Any]]): - "Compute Select options from topics list" - res = [] - for topic in topics: - res.append(discord.SelectOption(label=topic['topic'], value=topic['id'], emoji=topic['topic_emoji'])) - return res - -class SendHintText(discord.ui.View): - "Used to send a hint and make sure the user actually needs help" - def __init__(self, user_id: int, label_confirm: str, label_cancel: str, text_cancel: str): - super().__init__(timeout=180) - self.user_id = user_id - self.confirmed: Optional[bool] = None - self.interaction: Optional[discord.Interaction] = None - confirm_btn = discord.ui.Button(label=label_confirm, style=discord.ButtonStyle.green) - confirm_btn.callback = self.confirm - self.add_item(confirm_btn) - self.text_cancel = text_cancel - cancel_btn = discord.ui.Button(label=label_cancel, style=discord.ButtonStyle.red) - cancel_btn.callback = self.cancel - self.add_item(cancel_btn) - - async def interaction_check(self, interaction: discord.Interaction): - return interaction.user.id == self.user_id - - async def confirm(self, interaction: discord.Interaction): - "When user clicks on the confirm button" - self.confirmed = True - self.interaction = interaction - self.stop() - - async def cancel(self, interaction: discord.Interaction): - "When user clicks on the cancel button" - await interaction.response.defer() - self.confirmed = False - self.stop() - await self.disable(interaction) - await interaction.followup.send(self.text_cancel, ephemeral=True) - - async def disable(self, src: Union[discord.Interaction, discord.Message]): - "When the view timeouts or is disabled" - for child in self.children: - child.disabled = True - if isinstance(src, discord.Interaction): - await src.edit_original_response(view=self) - else: - await src.edit(content=src.content, embeds=src.embeds, view=self) - self.stop() - -class AskTitleModal(discord.ui.Modal): - "Ask a user the name of their ticket" - name = discord.ui.TextInput(label="", placeholder=None, style=discord.TextStyle.short, max_length=100) - - def __init__(self, guild_id: int, topic: dict, title: str, input_label: str, input_placeholder: str, callback: Callable): - super().__init__(title=title, timeout=600) - self.guild_id = guild_id - self.topic = topic - self.callback = callback - self.name.label = input_label - self.name.placeholder = input_placeholder - - async def on_submit(self, interaction: discord.Interaction): - try: - await interaction.response.defer(ephemeral=True, thinking=True) - await self.callback(interaction, self.topic, self.name.value.strip()) - except Exception as err: # pylint: disable=broad-except - interaction.client.dispatch("error", err, f"When opening a ticket in guild {self.guild_id}") - await interaction.edit_original_response(content="An error occured while opening your ticket. Please try again later.") - -class AskTopicSelect(discord.ui.View): - "Ask a user what topic they want to edit/delete" - def __init__(self, user_id: int, topics: list[dict[str, Any]], placeholder: str, max_values: int): - super().__init__() - self.user_id = user_id - options = self.build_options(topics) - self.select = discord.ui.Select(placeholder=placeholder, min_values=1, max_values=min(max_values, len(options)), options=options) - self.select.callback = self.callback - self.add_item(self.select) - self.topics: list[str] = None - - async def interaction_check(self, interaction: discord.Interaction): - return interaction.user.id == self.user_id - - def build_options(self, topics: list[dict[str, Any]]) -> list[discord.SelectOption]: - "Build the options list for Discord" - res = [] - for topic in topics: - res.append(discord.SelectOption(label=topic['topic'], value=topic['id'], emoji=topic['topic_emoji'])) - return res - - async def callback(self, interaction: discord.Interaction): - "Called when the dropdown menu has been validated by the user" - self.topics = self.select.values - await interaction.response.defer() - self.select.disabled = True - await interaction.edit_original_response(view=self) - self.stop() - class Tickets(commands.Cog): "Handle the bot tickets system" @@ -167,7 +54,8 @@ async def on_interaction(self, interaction: discord.Interaction): await interaction.response.send_message(await self.bot._(interaction.guild_id, "errors.unknown"), ephemeral=True) raise Exception(f"No topic found on guild {interaction.guild_id} with interaction {topic_id}") if topic['category'] is None: - await interaction.response.send_message(await self.bot._(interaction.guild_id, "tickets.missing-category-config"), ephemeral=True) + cmd = await self.bot.get_command_mention("tickets portal set-category") + await interaction.response.send_message(await self.bot._(interaction.guild_id, "tickets.missing-category-config", set_category=cmd), ephemeral=True) return if topic['hint']: hint_view = SendHintText(interaction.user.id, @@ -295,9 +183,11 @@ async def ask_user_topic(self, ctx: MyContext, multiple = False, message: Option "Ask a user which topic they want to edit" placeholder = await self.bot._(ctx.guild.id, "tickets.selection-placeholder") view = AskTopicSelect(ctx.author.id, await self.db_get_topics(ctx.guild.id), placeholder, 25 if multiple else 1) - await ctx.send(message or await self.bot._(ctx.guild.id, "tickets.choose-topic-edition"), view=view) + msg = await ctx.send(message or await self.bot._(ctx.guild.id, "tickets.choose-topic-edition"), view=view) await view.wait() if view.topics is None: + # timeout + await view.disable(msg) return None try: if multiple: @@ -417,28 +307,55 @@ async def create_ticket(self, interaction: discord.Interaction, topic: dict, tic await msg.pin() self.bot.dispatch("ticket_creation", TicketCreationEvent(topic, ticket_name, interaction, channel)) - - - @commands.group(name="ticket", aliases=["tickets"]) + async def topic_id_autocompletion(self, interaction: discord.Interaction, current: str, allow_other: bool=True): + "Autocompletion to select a topic in an app command" + current = current.lower() + topics = await self.db_get_topics(interaction.guild_id) + if allow_other: + topics.append({ + "id": -1, + "topic": (await self.bot._(interaction.guild_id, "tickets.other")).capitalize(), + "topic_emoji": None + }) + filtered = sorted([ + (not topic["topic"].lower().startswith(current), topic["topic"], topic["id"]) + for topic in topics + if current in topic["topic"].lower() + ]) + return [ + app_commands.Choice(name=name, value=topic_id) + for _, name, topic_id in filtered + ] + + async def send_error_message(self, ctx: MyContext): + about = await self.bot.get_command_mention("about") + await ctx.send(await self.bot._(ctx.guild.id, "errors.unknown", about=about)) + + @commands.hybrid_group(name="tickets", aliases=["ticket"]) + @discord.app_commands.default_permissions(manage_channels=True) + @commands.check(checks.has_manage_channels) @commands.guild_only() async def tickets_main(self, ctx: MyContext): """Manage your tickets system ..Doc tickets.html""" if ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx, ['tickets']) + await ctx.send_help(ctx.command) @tickets_main.group(name="portal") + @commands.guild_only() @commands.check(checks.has_manage_channels) async def tickets_portal(self, ctx: MyContext): """Handle how your members are able to open tickets ..Doc tickets.html""" if ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx, ['tickets', 'portal']) + await ctx.send_help(ctx.command) @tickets_portal.command() @commands.cooldown(2, 30, commands.BucketType.guild) + @commands.guild_only() + @commands.check(checks.has_manage_channels) async def summon(self, ctx: MyContext): """Ask the bot to send a message allowing people to open tickets @@ -450,10 +367,15 @@ async def summon(self, ctx: MyContext): } defaults = await self.db_get_defaults(ctx.guild.id) prompt = defaults["prompt"] if defaults else await self.bot._(ctx.guild.id, "tickets.default-topic-prompt") - await ctx.send(prompt, view=SelectView(ctx.guild.id, topics + [other])) + await ctx.channel.send(prompt, view=SelectView(ctx.guild.id, topics + [other])) + if ctx.interaction: + await ctx.reply(await self.bot._(ctx.guild.id, "misc.done!"), ephemeral=True) @tickets_portal.command(name="set-hint") + @app_commands.describe(message="The hint to display for this topic - set 'none' for no hint") @commands.cooldown(3, 30, commands.BucketType.guild) + @commands.guild_only() + @commands.check(checks.has_manage_channels) async def portal_set_hint(self, ctx: MyContext, *, message: str): """Set a default hint message The message will be displayed when a user tries to open a ticket, before user confirmation @@ -470,11 +392,14 @@ async def portal_set_hint(self, ctx: MyContext, *, message: str): if await self.db_edit_topic_hint(ctx.guild.id, row_id, message): await ctx.send(await self.bot._(ctx.guild.id, "tickets.hint-edited.default")) else: - await ctx.send(await self.bot._(ctx.guild.id, "errors.unknown")) + await self.send_error_message(ctx) @tickets_portal.command(name="set-role") + @app_commands.describe(role="The role allowed to see tickets from this topic - do not set to allow anyone") @commands.cooldown(3, 30, commands.BucketType.guild) - async def portal_set_role(self, ctx: MyContext, role: Union[discord.Role, Literal["none"]]): + @commands.guild_only() + @commands.check(checks.has_manage_channels) + async def portal_set_role(self, ctx: MyContext, role: Optional[discord.Role]): """Edit a default staff role Anyone with this role will be able to read newly created tickets Type "None" to set admins only @@ -485,13 +410,14 @@ async def portal_set_role(self, ctx: MyContext, role: Union[discord.Role, Litera row_id = await self.db_get_guild_default_id(ctx.guild.id) if row_id is None: row_id = await self.db_set_guild_default_id(ctx.guild.id) - if role == "none": - role = None await self.db_edit_topic_role(ctx.guild.id, row_id, role.id if role else None) - await ctx.send(await self.bot._(ctx.guild.id, "tickets.role-edited.default")) + key = "tickets.role-edited.default-reset" if role is None else "tickets.role-edited.default" + await ctx.send(await self.bot._(ctx.guild.id, key)) @tickets_portal.command(name="set-text") @commands.cooldown(3, 30, commands.BucketType.guild) + @commands.guild_only() + @commands.check(checks.has_manage_channels) async def portal_set_text(self, ctx: MyContext, *, message: str): """Set a message to be displayed above the ticket topic selection @@ -506,6 +432,8 @@ async def portal_set_text(self, ctx: MyContext, *, message: str): @tickets_portal.command(name="set-category", aliases=["set-channel"]) @commands.cooldown(3, 30, commands.BucketType.guild) + @commands.guild_only() + @commands.check(checks.has_manage_channels) async def portal_set_category(self, ctx: MyContext, category_or_channel: Union[discord.CategoryChannel, discord.TextChannel]): """Set the category or the channel in which tickets will be created @@ -540,8 +468,11 @@ async def portal_set_category(self, ctx: MyContext, category_or_channel: Union[d await ctx.send(message, embed=embed) @tickets_portal.command(name="set-format") + @app_commands.describe(name_format="The channel format for this topic - set 'none' to use the default one") @commands.cooldown(3, 30, commands.BucketType.guild) - async def portal_set_format(self, ctx: MyContext, name_format: Union[Literal["none"], str]): + @commands.guild_only() + @commands.check(checks.has_manage_channels) + async def portal_set_format(self, ctx: MyContext, name_format: str): """Set the format used to generate the channel/thread name You can use the following placeholders: username, usertag, userid, topic, topic_emoji, ticket_name Use "none" to reset the format to the default one @@ -563,17 +494,20 @@ async def portal_set_format(self, ctx: MyContext, name_format: Union[Literal["no @tickets_main.group(name="topic", aliases=["topics"]) + @commands.guild_only() @commands.check(checks.has_manage_channels) async def tickets_topics(self, ctx: MyContext): """Handle the different ticket topics your members can select ..Doc tickets.html""" if ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx, ['tickets', 'topic']) + await ctx.send_help(ctx.command) @tickets_topics.command(name="add", aliases=["create"]) @commands.cooldown(3, 45, commands.BucketType.guild) - async def topic_add(self, ctx: MyContext, emote: Union[discord.PartialEmoji, UnicodeEmoji, None]=None, *, name: str): + @commands.guild_only() + @commands.check(checks.has_manage_channels) + async def topic_add(self, ctx: MyContext, emote: Optional[EmojiConverterType]=None, *, name: str): """Create a new ticket topic A topic name is limited to 100 characters Only Discord emojis are accepted for now @@ -601,13 +535,20 @@ async def topic_add(self, ctx: MyContext, emote: Union[discord.PartialEmoji, Uni @tickets_topics.command(name="remove") @commands.cooldown(3, 45, commands.BucketType.guild) - async def topic_remove(self, ctx: MyContext): + @commands.guild_only() + @commands.check(checks.has_manage_channels) + async def topic_remove(self, ctx: MyContext, topic_id: Optional[int] = None): """Permanently delete a topic by its name ..Doc tickets.html#delete-a-topic""" - topic_ids: Optional[list[int]] = await self.ask_user_topic(ctx, True, await ctx.bot._(ctx.guild.id, "tickets.choose-topics-deletion")) - if topic_ids is None or len(topic_ids) == 0: - await ctx.send(await self.bot._(ctx.guild.id, "errors.unknown")) + if not topic_id or not await self.db_topic_exists(ctx.guild.id, topic_id): + topic_id = await self.ask_user_topic(ctx, True, await ctx.bot._(ctx.guild.id, "tickets.choose-topics-deletion")) + if topic_id is None: + # timeout + return + topic_ids: Optional[list[int]] = [topic_id] + if topic_ids is None: + # timeout return if len(topic_ids) == 1: topic = await self.db_get_topic_with_defaults(ctx.guild.id, topic_ids[0]) @@ -618,12 +559,19 @@ async def topic_remove(self, ctx: MyContext): if counter > 0: await ctx.send(await self.bot._(ctx.guild.id, "tickets.topic.deleted", count=counter, name=topic_name)) else: - await ctx.send(await self.bot._(ctx.guild.id, "errors.unknown")) + await self.send_error_message(ctx) + + @topic_remove.autocomplete("topic_id") + async def topic_remove_autocomplete(self, interaction: discord.Interaction, current: str): + return await self.topic_id_autocompletion(interaction, current, allow_other=False) + @tickets_topics.command(name="set-emote", aliases=["set-emoji"]) @commands.cooldown(3, 30, commands.BucketType.guild) + @commands.guild_only() + @commands.check(checks.has_manage_channels) async def topic_set_emote(self, ctx: MyContext, topic_id: Optional[int], - emote: Union[discord.PartialEmoji, UnicodeEmoji, Literal["none"]]): + emote: Optional[EmojiConverterType]=None): """Edit a topic emoji Type "None" to set no emoji for this topic @@ -633,10 +581,8 @@ async def topic_set_emote(self, ctx: MyContext, topic_id: Optional[int], if not topic_id or not await self.db_topic_exists(ctx.guild.id, topic_id): topic_id = await self.ask_user_topic(ctx) if topic_id is None: - await ctx.send(await self.bot._(ctx.guild.id, "errors.unknown")) + # timeout return - if emote == "none": - emote = None elif isinstance(emote, discord.PartialEmoji): emote = f"{emote.name}:{emote.id}" if await self.db_edit_topic_emoji(ctx.guild.id, topic_id, emote): @@ -644,10 +590,17 @@ async def topic_set_emote(self, ctx: MyContext, topic_id: Optional[int], await ctx.send(await self.bot._(ctx.guild.id, "tickets.emote-edited", topic=topic["topic"])) else: await ctx.send(await self.bot._(ctx.guild.id, "tickets.nothing-to-edit")) + + @topic_set_emote.autocomplete("topic_id") + async def topic_set_emote_autocomplete(self, interaction: discord.Interaction, current: str): + return await self.topic_id_autocompletion(interaction, current, allow_other=False) @tickets_topics.command(name="set-hint") + @app_commands.describe(message="The hint to display for this topic - set 'none' to use the default one") @commands.cooldown(3, 30, commands.BucketType.guild) - async def topic_set_hint(self, ctx: MyContext, topic_id: Optional[int], *, message: str): + @commands.guild_only() + @commands.check(checks.has_manage_channels) + async def topic_set_hint(self, ctx: MyContext, topic_id: Optional[int]=None, *, message: str): """Edit a topic hint message The message will be displayed when a user tries to open a ticket, before user confirmation Type "None" to set the text to the default one (`tickets portal set-hint`) @@ -659,17 +612,24 @@ async def topic_set_hint(self, ctx: MyContext, topic_id: Optional[int], *, messa if not topic_id or not await self.db_topic_exists(ctx.guild.id, topic_id): topic_id = await self.ask_user_topic(ctx) if topic_id is None: - await ctx.send(await self.bot._(ctx.guild.id, "errors.unknown")) + # timeout return if message.lower() == "none": message = None await self.db_edit_topic_hint(ctx.guild.id, topic_id, message) topic = await self.db_get_topic_with_defaults(ctx.guild.id, topic_id) await ctx.send(await self.bot._(ctx.guild.id, "tickets.hint-edited.topic", topic=topic["topic"])) + + @topic_set_hint.autocomplete("topic_id") + async def topic_set_hint_autocomplete(self, interaction: discord.Interaction, current: str): + return await self.topic_id_autocompletion(interaction, current) @tickets_topics.command(name="set-role") + @app_commands.describe(role="The role allowed to see tickets from this topic - do not set to use the default one") @commands.cooldown(3, 30, commands.BucketType.guild) - async def topic_set_role(self, ctx: MyContext, topic_id: Optional[int], role: Union[discord.Role, Literal["none"]]): + @commands.guild_only() + @commands.check(checks.has_manage_channels) + async def topic_set_role(self, ctx: MyContext, topic_id: Optional[int]=None, role: Optional[discord.Role]=None): """Edit a topic staff role Anyone with this role will be able to read newly created tickets with this topic Type "None" to set the role to the default one (`tickets portal set-role`) @@ -682,19 +642,25 @@ async def topic_set_role(self, ctx: MyContext, topic_id: Optional[int], role: Un if not topic_id or not await self.db_topic_exists(ctx.guild.id, topic_id): topic_id = await self.ask_user_topic(ctx) if topic_id is None: - await ctx.send(await self.bot._(ctx.guild.id, "errors.unknown")) + # timeout return - if role.lower() == "none": - role = None if await self.db_edit_topic_role(ctx.guild.id, topic_id, role.id if role else None): topic = await self.db_get_topic_with_defaults(ctx.guild.id, topic_id) - await ctx.send(await self.bot._(ctx.guild.id, "tickets.role-edited.topic", topic=topic["topic"])) + key = "tickets.role-edited.topic-reset" if role is None else "tickets.role-edited.topic" + await ctx.send(await self.bot._(ctx.guild.id, key, topic=topic["topic"])) else: await ctx.send(await self.bot._(ctx.guild.id, "tickets.nothing-to-edit")) + + @topic_set_role.autocomplete("topic_id") + async def topic_set_role_autocomplete(self, interaction: discord.Interaction, current: str): + return await self.topic_id_autocompletion(interaction, current) @tickets_topics.command(name="set-format") + @app_commands.describe(name_format="The channel format for this topic - set 'none' to use the default one") @commands.cooldown(3, 30, commands.BucketType.guild) - async def topic_set_role(self, ctx: MyContext, topic_id: Optional[int], name_format: Union[Literal["none"], str]): + @commands.guild_only() + @commands.check(checks.has_manage_channels) + async def topic_set_format(self, ctx: MyContext, topic_id: Optional[int], name_format: str): """Set the format used to generate the channel/thread name You can use the following placeholders: username, usertag, userid, topic, topic_emoji, ticket_name Use "none" to reset the format to the default one @@ -706,7 +672,7 @@ async def topic_set_role(self, ctx: MyContext, topic_id: Optional[int], name_for if not topic_id or not await self.db_topic_exists(ctx.guild.id, topic_id): topic_id = await self.ask_user_topic(ctx) if topic_id is None: - await ctx.send(await self.bot._(ctx.guild.id, "errors.unknown")) + # timeout return if len(name_format) > self.max_format_length: await ctx.send(await self.bot._(ctx.guild.id, "tickets.format.too-long", max=self.max_format_length)) @@ -718,11 +684,17 @@ async def topic_set_role(self, ctx: MyContext, topic_id: Optional[int], name_for await ctx.send(await self.bot._(ctx.guild.id, "tickets.format.edited.topic", topic=topic["topic"])) else: await ctx.send(await self.bot._(ctx.guild.id, "tickets.nothing-to-edit")) + + @topic_set_format.autocomplete("topic_id") + async def topic_set_format_autocomplete(self, interaction: discord.Interaction, current: str): + return await self.topic_id_autocompletion(interaction, current) @tickets_topics.command(name="list") @commands.cooldown(3, 20, commands.BucketType.guild) - async def topic_set_roles(self, ctx: MyContext): + @commands.guild_only() + @commands.check(checks.has_manage_channels) + async def topic_list(self, ctx: MyContext): "List every ticket topic used in your server" topics_repr: list[str] = [] none_emoji: str = self.bot.emojis_manager.customs['nothing'] diff --git a/fcts/timers.py b/fcts/timers.py index 7ff26b26..7d82eb8d 100644 --- a/fcts/timers.py +++ b/fcts/timers.py @@ -81,7 +81,7 @@ async def remind_main(self, ctx: MyContext): ..Doc miscellaneous.html#reminders""" if ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx,['reminder']) + await ctx.send_help(ctx.command) @remind_main.command(name="create", aliases=["add"]) @@ -113,7 +113,7 @@ async def remind_create(self, ctx: MyContext, duration: commands.Greedy[args.tem d = {'msg_url': ctx.message.jump_url} await ctx.bot.task_handler.add_task("timer", duration, ctx.author.id, ctx.guild.id if ctx.guild else None, ctx.channel.id, message, data=d) except Exception as err: - await ctx.bot.get_cog("Errors").on_command_error(ctx, err) + self.bot.dispatch("command_error", ctx, err) else: await ctx.send(await self.bot._(ctx.channel, "timers.rmd.saved", duration=f_duration)) @@ -234,12 +234,16 @@ async def ask_reminder_ids(self, input_id: Optional[int], ctx: MyContext, title: await view.disable(msg) return try: - selection = list(map(int, view.values)) + if isinstance(view.values, str): + selection = [int(view.values)] + else: + selection = list(map(int, view.values)) except ValueError: selection = [] self.bot.dispatch("error", ValueError(f"Invalid reminder IDs: {view.values}"), ctx) if len(selection) == 0: - await ctx.send(await self.bot._(ctx.guild, "rss.fail-add")) + cmd = await self.bot.get_command_mention("about") + await ctx.send(await self.bot._(ctx.guild, "errors.unknown2", about=cmd)) return return selection @@ -261,7 +265,13 @@ async def remind_del(self, ctx: MyContext, reminder_id: Optional[int] = None): if await self.db_delete_reminders(ids, ctx.author.id): for reminder_id in ids: await self.bot.task_handler.remove_task(reminder_id) - await ctx.send(await self.bot._(ctx.channel, "timers.rmd.delete.success", count=len(ids))) + await ctx.send(await self.bot._(ctx.channel, "timers.rmd.delete.success", count=len(ids))) + else: + await ctx.send(await self.bot._(ctx.channel, "timers.rmd.delete.error")) + try: + raise ValueError(f"Failed to delete reminders: {ids}") + except ValueError as err: + self.bot.dispatch("error", err, ctx) @remind_main.command(name="clear") @commands.cooldown(3, 60, commands.BucketType.user) @@ -281,9 +291,10 @@ async def remind_clear(self, ctx: MyContext): validation=lambda inter: inter.user==ctx.author, timeout=20) await confirm_view.init() - await ctx.send(await self.bot._(ctx.channel, "timers.rmd.confirm", count=count), view=confirm_view) + confirm_msg = await ctx.send(await self.bot._(ctx.channel, "timers.rmd.confirm", count=count), view=confirm_view) await confirm_view.wait() + await confirm_view.disable(confirm_msg) if confirm_view.value is None: await ctx.send(await self.bot._(ctx.channel, "timers.rmd.cancelled")) return diff --git a/fcts/translations.py b/fcts/translations.py index 3e8fcdaa..74bef15b 100644 --- a/fcts/translations.py +++ b/fcts/translations.py @@ -235,7 +235,7 @@ async def translate_main(self, ctx: MyContext): cmd = self.bot.get_command("translators translate") await cmd(ctx, ctx.subcommand_passed) elif ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx, ['tr']) + await ctx.send_help(ctx.command) @translate_main.command(name="translate") async def translate_smth(self, ctx: MyContext, lang: typing.Optional[LanguageId] = None): @@ -245,7 +245,7 @@ async def translate_smth(self, ctx: MyContext, lang: typing.Optional[LanguageId] Use no argument to get a help message""" if lang is None: - await self.bot.get_cog('Help').help_command(ctx, ['translators']) + await ctx.send_help(ctx.command) return if lang not in languages or lang == 'en': return await ctx.send("Invalid language") diff --git a/fcts/twitch.py b/fcts/twitch.py new file mode 100644 index 00000000..bc36482f --- /dev/null +++ b/fcts/twitch.py @@ -0,0 +1,386 @@ +import json +import re +from typing import Optional, TypedDict + +import discord +from dateutil.parser import isoparse +from discord import app_commands +from discord.ext import commands, tasks +from mysql.connector.errors import IntegrityError + +from . import checks +from libs.bot_classes import MyContext, Zbot +from libs.twitch.api_agent import TwitchApiAgent +from libs.twitch.types import (GroupedStreamerDBObject, PlatformId, + StreamersDBObject, StreamObject) + + +class _StreamersReadyForNotification(TypedDict): + platform: PlatformId + user_id: str + user_name: str + is_streaming: bool + guilds: list[discord.Guild] + +class Twitch(commands.Cog): + "Handle twitch streams" + + def __init__(self, bot: Zbot): + self.bot = bot + self.file = "twitch" + self.agent = TwitchApiAgent() + self.twitch_color = 0x6441A4 + + async def cog_load(self): + await self.agent.api_login( + self.bot.others["twitch_client_id"], + self.bot.others["twitch_client_secret"] + ) + self.bot.log.info("[twitch] connected to API") + self.stream_check_task.start() + + async def cog_unload(self): + "Close the Twitch session" + await self.agent.close_session() + self.bot.log.info("[twitch] connection closed") + self.stream_check_task.cancel() + + async def db_add_streamer(self, guild_id: int, platform: PlatformId, user_id: str, user_name: str): + "Add a streamer to the database" + query = "INSERT INTO `streamers` (`guild_id`, `platform`, `user_id`, `user_name`, `beta`) VALUES (%s, %s, %s, %s, %s)" + try: + async with self.bot.db_query(query, (guild_id, platform, user_id, user_name, self.bot.beta), returnrowcount=True) as query_result: + return query_result > 0 + except IntegrityError: + return False + + async def db_get_guild_subscriptions_count(self, guild_id: int) -> Optional[int]: + "Get the number of subscriptions for a guild" + query = "SELECT COUNT(*) FROM `streamers` WHERE `guild_id` = %s AND `beta` = %s" + async with self.bot.db_query(query, (guild_id, self.bot.beta), astuple=True) as query_result: + return query_result[0][0] if query_result else None + + async def db_get_guild_streamers(self, guild_id: int, platform: Optional[PlatformId]=None) -> list[StreamersDBObject]: + "Get the streamers for a guild" + query = "SELECT * FROM `streamers` WHERE `guild_id` = %s AND `beta` = %s" + args = [guild_id, self.bot.beta] + if platform is not None: + query += " AND `platform` = %s" + args.append(platform) + async with self.bot.db_query(query, args) as query_result: + return query_result + + async def db_get_guilds_per_streamers(self, platform: Optional[PlatformId]=None) -> list[GroupedStreamerDBObject]: + "Get all streamers objects" + where = "" if platform is None else f"AND `platform` = \"{platform}\"" + query = f"SELECT `platform`, `user_id`, `user_name`, `is_streaming`, JSON_ARRAYAGG(`guild_id`) as \"guild_ids\" FROM `streamers` WHERE `beta` = %s {where} GROUP BY `platform`, `user_id`; " + async with self.bot.db_query(query, (self.bot.beta,)) as query_result: + return [ + data | {"guild_ids": json.loads(data["guild_ids"])} + for data in query_result + ] + + async def db_remove_streamer(self, guild_id: int, platform: PlatformId, user_id: str): + "Remove a streamer from the database" + query = "DELETE FROM `streamers` WHERE `guild_id` = %s AND `platform` = %s AND `user_id` = %s AND `beta` = %s" + async with self.bot.db_query(query, (guild_id, platform, user_id, self.bot.beta), returnrowcount=True) as query_result: + return query_result > 0 + + async def db_set_streamer_status(self, platform: PlatformId, user_id: str, is_streaming: bool): + "Set the streaming status of a streamer" + query = "UPDATE `streamers` SET `is_streaming` = %s WHERE `platform` = %s AND `user_id` = %s AND `beta` = %s" + async with self.bot.db_query(query, (is_streaming, platform, user_id, self.bot.beta), returnrowcount=True) as query_result: + return query_result > 0 + + async def db_get_streamer_status(self, platform: PlatformId, user_id: str) -> Optional[bool]: + "Get the streaming status of a streamer" + query = "SELECT `is_streaming` FROM `streamers` WHERE `platform` = %s AND `user_id` = %s AND `beta` = %s LIMIT 1" + async with self.bot.db_query(query, (platform, user_id, self.bot.beta), astuple=True) as query_result: + return query_result[0][0] if query_result else None + + async def db_get_streamer_name(self, platform: PlatformId, user_id: str) -> Optional[str]: + "Get the last known name of a streamer from its ID and platform" + query = "SELECT `user_name` FROM `streamers` WHERE `platform` = %s AND `user_id` = %s AND `beta` = %s LIMIT 1" + async with self.bot.db_query(query, (platform, user_id, self.bot.beta), astuple=True) as query_result: + return query_result[0][0] if query_result else None + + @commands.hybrid_group(name="twitch") + @app_commands.default_permissions(manage_guild=True) + @commands.guild_only() + @commands.check(checks.has_manage_guild) + async def twitch(self, ctx: MyContext): + """Twitch commands + +..Doc streamers.html""" + if ctx.invoked_subcommand is None: + await ctx.send_help(ctx.command) + + @twitch.command(name="subscribe") + @commands.guild_only() + @commands.check(checks.has_manage_guild) + async def twitch_sub(self, ctx: MyContext, streamer: str): + """Subscribe to a Twitch streamer + +..Example twitch subscribe https://twitch.tv/monstercat + +..Example twitch subscribe Zerator + +..Doc streamers.html#subscribe-or-unsubscribe-to-a-streamer""" + await ctx.defer() + if match := re.findall(r'^https://(?:www\.)?twitch\.tv/(\w+)', streamer): + streamer = match[0] + try: + user = await self.agent.get_user_by_name(streamer) + except ValueError: + await ctx.send(await self.bot._(ctx.guild.id, "twitch.invalid-streamer-name")) + return + if user is None: + await ctx.send(await self.bot._(ctx.guild.id, "twitch.unknown-streamer")) + return + streamers_count = await self.db_get_guild_subscriptions_count(ctx.guild.id) + max_count = await self.bot.get_config(ctx.guild.id,'streamers_max_number') + if streamers_count >= max_count: + await ctx.send(await self.bot._(ctx.guild.id, "twitch.subscribe.limit-reached", max_count)) + return + if await self.db_add_streamer(ctx.guild.id, "twitch", user["id"], user["display_name"]): + await ctx.send(await self.bot._(ctx.guild.id, "twitch.subscribe.success", streamer=user["display_name"])) + else: + await ctx.send(await self.bot._(ctx.guild.id, "twitch.subscribe.already-subscribed", streamer=user["display_name"])) + + @twitch.command(name="unsubscribe") + @commands.guild_only() + @commands.check(checks.has_manage_guild) + async def twitch_unsub(self, ctx: MyContext, streamer: str): + """Unsubscribe from a Twitch streamer + +..Example twitch unsubscribe monstercat + +..Doc streamers.html#subscribe-or-unsubscribe-to-a-streamer""" + if streamer.isnumeric(): + user_id = streamer + user_name = await self.db_get_streamer_name("twitch", user_id) + else: + if user := await self.agent.get_user_by_name(streamer): + user_id = user["id"] + user_name = user["display_name"] + else: + await ctx.send(await self.bot._(ctx.guild.id, "twitch.unknown-streamer")) + return + if await self.db_remove_streamer(ctx.guild.id, "twitch", user_id): + await ctx.send(await self.bot._(ctx.guild.id, "twitch.unsubscribe.success", streamer=user_name)) + else: + await ctx.send(await self.bot._(ctx.guild.id, "twitch.unsubscribe.not-subscribed", streamer=user_name)) + + @twitch_unsub.autocomplete("streamer") + async def twitch_unsub_autocomplete(self, ctx: MyContext, current: str): + "Autocomplete for twitch_unsub" + current = current.lower() + streamers = await self.db_get_guild_streamers(ctx.guild.id, "twitch") + filtered = [ + (not streamer["user_name"].lower().startswith(current), streamer["user_name"], streamer["user_id"]) + for streamer in streamers + if current in streamer["user_name"].lower() or current == streamer["user_id"] + ] + filtered.sort() + return [ + app_commands.Choice(name=name, value=value) + for _, name, value in filtered + ] + + @twitch.command(name="list-subscriptions") + @commands.guild_only() + @commands.check(checks.has_manage_guild) + async def twitch_list(self, ctx: MyContext): + """List all subscribed Twitch streamers + +..Example twitch list-subscriptions + +..Doc streamers.html#list-your-subscriptions""" + await ctx.defer() + streamers = await self.db_get_guild_streamers(ctx.guild.id, "twitch") + max_count = await self.bot.get_config(ctx.guild.id,'streamers_max_number') + if streamers: + title = await self.bot._(ctx.guild.id, "twitch.subs-list.title", current=len(streamers), max=max_count) + on_live = await self.bot._(ctx.guild.id, "twitch.on-live-indication") + streamers_name = [ + streamer["user_name"] + (f" *({on_live})*" if streamer["is_streaming"] else "") + for streamer in streamers + ] + streamers_name.sort(key=str.casefold) + await ctx.send(title+"\n• " + "\n• ".join(streamers_name)) + else: + txt = await self.bot._(ctx.guild.id, "twitch.subs-list.empty", max=max_count) + cmd = await self.bot.get_command_mention("twitch subscribe") + txt += "\n" + await self.bot._(ctx.guild.id, "twitch.subs-list.empty-tip", cmd=cmd, max=max_count) + await ctx.send(txt) + + @twitch.command(name="check-stream") + @commands.cooldown(3, 60, commands.BucketType.user) + async def test_twitch(self, ctx: MyContext, streamer: str): + """Check if a streamer is currently streaming + +..Example twitch check-stream monstercat + +..Doc streamers.html#check-a-streamer-status""" + if streamer.isnumeric(): + user_id = streamer + avatar = None + else: + try: + streamer_obj = await self.agent.get_user_by_name(streamer) + avatar = streamer_obj["profile_image_url"].format(width=64, height=64) + user_id = streamer_obj["id"] + except ValueError: + await ctx.send(await self.bot._(ctx, "twitch.invalid-streamer-name")) + return + resp = await self.agent.get_user_stream_by_id(user_id) + if len(resp) > 0: + stream = resp[0] + if stream["is_mature"] and not (ctx.guild is None or ctx.channel.is_nsfw()): + await ctx.send(await self.bot._(ctx, "twitch.check-stream.no-nsfw")) + return + await ctx.send(embed=await self.create_stream_embed(stream, ctx, avatar)) + else: + await ctx.send(await self.bot._(ctx, "twitch.check-stream.offline", streamer=streamer)) + + async def create_stream_embed(self, stream: StreamObject, guild_id: int, streamer_avatar: Optional[str]=None): + started_at = isoparse(stream["started_at"]) + embed = discord.Embed( + title=stream["title"], + url=f"https://twitch.tv/{stream['user_name']}", + color=self.twitch_color, + timestamp=started_at, + description=await self.bot._(guild_id, "twitch.check-stream.category", game=stream['game_name']) + ) + embed.set_image(url=stream["thumbnail_url"].format(width=1280, height=720)) + embed.set_author(name=stream["user_name"], url=f"https://twitch.tv/{stream['user_name']}", icon_url=streamer_avatar) + return embed + + async def find_streamer_in_guild(self, streamer_name: str, guild: discord.Guild): + "Try to find a member currently streaming with this streamer name in the given guild" + streamer_name = streamer_name.lower() + for member in guild.members: + if member.bot or not member.activities: + continue + for activity in member.activities: + if activity.type != discord.ActivityType.streaming: + continue + if activity.twitch_name and activity.twitch_name.lower() == streamer_name: + return member + + async def find_streamer_offline_in_guild(self, guild: discord.Guild, role: discord.Role): + "Find any member in the guild who has the streamer role but is not currently streaming" + for member in guild.members: + if role in member.roles and not any(activity.type == discord.ActivityType.streaming for activity in member.activities): + yield member + + async def send_stream_alert(self, stream: StreamObject, channel: discord.abc.GuildChannel): + "Send a stream alert to a guild when a streamer is live" + msg = await self.bot._(channel.guild, "twitch.stream-alerts", streamer=stream["user_name"]) + allowed_mentions = discord.AllowedMentions.none() + if role_id := await self.bot.get_config(channel.guild.id, "stream_mention"): + if role := channel.guild.get_role(int(role_id)): + msg = role.mention + " " + msg + allowed_mentions = discord.AllowedMentions(roles=[role]) + if channel.permissions_for(channel.guild.me).embed_links: + if streamer := await self.agent.get_user_by_id(stream["user_id"]): + avatar = streamer["profile_image_url"].format(width=64, height=64) + else: + avatar = None + embed = await self.create_stream_embed(stream, channel.guild.id, avatar) + else: + embed = None + msg += f"\nhttps://twitch.tv/{stream['user_name']}" + await channel.send( + msg, + embed=embed, + allowed_mentions=allowed_mentions + ) + + + @commands.Cog.listener() + async def on_stream_starts(self, stream: StreamObject, guild: discord.Guild): + "When a stream starts, send a notification to the subscribed guild" + # Send notification + if (channel_id := await self.bot.get_config(guild.id, "streaming_channel")) and channel_id.isnumeric(): + if channel := guild.get_channel(int(channel_id)): + await self.send_stream_alert(stream, channel) + else: + self.bot.log.warn("[twitch] Channel %s not found in guild %s", channel_id, guild.id) + # Grant role + if (role_id := await self.bot.get_config(guild.id, "streaming_role")) and role_id.isnumeric(): + if role := guild.get_role(int(role_id)): + if member := await self.find_streamer_in_guild(stream["user_name"], guild): + try: + await member.add_roles(role, reason="Twitch streamer is live") + except discord.Forbidden: + self.bot.log.info("[twitch] Cannot add role %s to member %s in guild %s: Forbidden", role_id, member.id, guild.id) + else: + self.bot.log.warn("[twitch] Role %s not found in guild %s", role_id, guild.id) + + @commands.Cog.listener() + async def on_stream_ends(self, streamer_name: str, guild: discord.Guild): + "When a stream ends, remove the role from the streamer" + if (role_id := await self.bot.get_config(guild.id, "streaming_role")) and role_id.isnumeric(): + if role := guild.get_role(int(role_id)): + async for member in self.find_streamer_offline_in_guild(guild, role): + try: + await member.remove_roles(role, reason="Twitch streamer is offline") + except discord.Forbidden: + self.bot.log.info("[twitch] Cannot remove role %s from member %s in guild %s: Forbidden", role_id, member.id, guild.id) + else: + self.bot.log.warn("[twitch] Role %s not found in guild %s", role_id, guild.id) + + @tasks.loop(minutes=5) + async def stream_check_task(self): + "Check if any subscribed streamer is streaming and send notifications" + await self.bot.wait_until_ready() + count = 0 + streamer_ids: dict[str, _StreamersReadyForNotification] = {} + for streamer in await self.db_get_guilds_per_streamers("twitch"): + # fetch guilds that need to be notified + guilds = [self.bot.get_guild(guild_id) for guild_id in streamer["guild_ids"]] + # remove unfound guilds and guilds where axobot i s + guilds = [ + guild + for guild in guilds + if guild is not None and not await self.bot.check_axobot_presence(guild=guild) + ] + if not guilds: # if not guild has been found, skip + continue + streamer_ids[streamer["user_id"]] = streamer | {"guilds": guilds} + count += 1 + # make one request every 30 streamers + if len(streamer_ids) > 30: + await self._update_streams(streamer_ids) + streamer_ids = {} + if streamer_ids: + await self._update_streams(streamer_ids) + self.bot.log.info("[twitch] %s streamers checked", count) + + async def _update_streams(self, streamer_ids: dict[str, _StreamersReadyForNotification]): + streaming_user_ids: set[str] = set() + # Check current streams + for stream in await self.agent.get_user_stream_by_id(*streamer_ids.keys()): + streamer_data = streamer_ids[stream["user_id"]] + # mark that this streamer is streaming + streaming_user_ids.add(stream["user_id"]) + # if it was already notified, skip + if streamer_data["is_streaming"]: + continue + # dispatch event + for guild in streamer_data["guilds"]: + self.bot.dispatch("stream_starts", stream, guild) + # mark streamers as streaming + await self.db_set_streamer_status("twitch", stream["user_id"], True) + # Check streamers that went offline + for streamer_id, streamer_data in streamer_ids.items(): + if streamer_id not in streaming_user_ids and streamer_data["is_streaming"]: + for guild in streamer_data["guilds"]: + self.bot.dispatch("stream_ends", streamer_data["user_name"], guild) + # mark streamers as offline + await self.db_set_streamer_status("twitch", streamer_id, False) + + +async def setup(bot: Zbot): + await bot.add_cog(Twitch(bot)) \ No newline at end of file diff --git a/fcts/users.py b/fcts/users.py index 0e6664d8..788f94e2 100644 --- a/fcts/users.py +++ b/fcts/users.py @@ -32,7 +32,7 @@ async def get_userflags(self, user: discord.User) -> list[str]: return False parameters = await get_data(criters=["userID="+str(user.id)], columns=['user_flags']) except Exception as err: - await self.bot.get_cog("Errors").on_error(err, None) + self.bot.dispatch("error", err) if parameters is None: return [] return UserFlag().int_to_flags(parameters['user_flags']) @@ -55,7 +55,7 @@ async def get_rankcards(self, user: discord.User) -> list[str]: return [] parameters = await get_data(criters=["userID="+str(user.id)], columns=['rankcards_unlocked']) except Exception as err: - await self.bot.get_cog("Errors").on_error(err, None) + self.bot.dispatch("error", err) if parameters is None: return [] return RankCardsFlag().int_to_flags(parameters['rankcards_unlocked']) @@ -94,7 +94,7 @@ async def get_rankcards_stats(self) -> dict: async with self.bot.db_query(query, astuple=True) as query_results: result = {x[0]: x[1] for x in query_results} except Exception as err: - await self.bot.get_cog("Errors").on_error(err, None) + self.bot.dispatch("error", err) return {} if '' in result: result['default'] = result.pop('') @@ -131,7 +131,7 @@ async def reload_event_rankcard(self, user: typing.Union[discord.User, int], car async def profile_main(self, ctx: MyContext): """Get and change info about yourself""" if ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx,['profile']) + await ctx.send_help(ctx.command) @profile_main.command(name='card-preview') @commands.check(checks.database_connected) @@ -167,7 +167,7 @@ async def profile_card(self, ctx: MyContext, style: typing.Optional[args.cardSty try: await self.reload_event_rankcard(ctx.author.id) except Exception as err: - await self.bot.get_cog("Errors").on_error(err, None) + self.bot.dispatch("error", err) await ctx.send(await self.bot._(ctx.channel, 'users.list-cards', cards=available_cards)) else: await ctx.send(await self.bot._(ctx.channel, 'users.invalid-card', cards=available_cards)) diff --git a/fcts/utilities.py b/fcts/utilities.py index f566d647..2b463571 100644 --- a/fcts/utilities.py +++ b/fcts/utilities.py @@ -41,7 +41,7 @@ async def get_bot_infos(self): return None async def find_img(self, name: str): - return discord.File(f"../images/{name}") + return discord.File(f"assets/images/{name}") async def find_url_redirection(self, url: str) -> str: """Find where an url is redirected to""" @@ -56,6 +56,10 @@ async def find_url_redirection(self, url: str) -> str: return str(err.args[0].real_url) except (asyncio.exceptions.TimeoutError, aiohttp.ServerTimeoutError): return url + except ValueError as err: + if err.args[0] != "URL should be absolute": + self.bot.dispatch("error", err) + return url return answer async def global_check(self, ctx: MyContext): @@ -65,7 +69,7 @@ async def global_check(self, ctx: MyContext): return True return False if await self.bot.check_axobot_presence(ctx=ctx): - if ctx.prefix.strip() == self.bot.user.mention: + if ctx.prefix and ctx.prefix.strip() == self.bot.user.mention: invite = 'http://discord.gg/N55zY88' await ctx.send(await self.bot._(ctx.guild.id, "errors.zbot-migration", invite=invite)) return False @@ -161,8 +165,8 @@ async def change_db_userinfo(self, user_id: int, key: str, value): async with self.bot.db_query(query, {'u': user_id, 'v': value}): pass return True - except Exception as e: - await self.bot.get_cog('Errors').on_error(e, None) + except Exception as err: + self.bot.dispatch("error", err) return False async def get_number_premium(self): @@ -170,15 +174,15 @@ async def get_number_premium(self): try: params = await self.get_db_userinfo(criters=['Premium=1']) return len(params) - except Exception as e: - await self.bot.get_cog('Errors').on_error(e, None) + except Exception as err: + self.bot.dispatch("error", err) async def get_xp_style(self, user: discord.User) -> str: parameters = None try: parameters = await self.get_db_userinfo(criters=["userID="+str(user.id)], columns=['xp_style']) - except Exception as e: - await self.bot.get_cog("Errors").on_error(e, None) + except Exception as err: + self.bot.dispatch("error", err) if parameters is None or parameters['xp_style'] == '': return 'dark' return parameters['xp_style'] @@ -244,10 +248,10 @@ async def add_user_eventPoint(self, user_id: int, points: int, override: bool = try: await self.bot.get_cog("Users").reload_event_rankcard(user_id) except Exception as err: - await self.bot.get_cog("Errors").on_error(err, None) + self.bot.dispatch("error", err) return True except Exception as err: - await self.bot.get_cog('Errors').on_error(err, None) + self.bot.dispatch("error", err) return False async def get_eventsPoints_rank(self, user_id: int): @@ -284,7 +288,7 @@ async def check_votes(self, userid: int) -> list[tuple[str, str]]: if json["voted"]: votes.append(("Discord Bots List", "https://top.gg/")) except Exception as err: - await self.bot.get_cog("Errors").on_error(err, None) + self.bot.dispatch("error", err) return votes diff --git a/fcts/welcomer.py b/fcts/welcomer.py index b1e9b551..5048573d 100644 --- a/fcts/welcomer.py +++ b/fcts/welcomer.py @@ -75,7 +75,7 @@ async def send_msg(self, member:discord.Member, event_type:str): for channel in channels: if not channel.isnumeric(): continue - channel = member.guild.get_channel(int(channel)) + channel = member.guild.get_channel_or_thread(int(channel)) if channel is None: continue botormember = await self.bot._(member.guild,"misc.bot" if member.bot else "misc.member") @@ -262,8 +262,8 @@ async def give_roles(self, member: discord.Member): await self.bot.get_cog('Events').send_logs_per_server(member.guild,'error',await self.bot._(member.guild, "welcome.error-give-roles",r=role.name, u=str(member)), member.guild.me) except discord.errors.NotFound: pass - except Exception as e: - await self.bot.get_cog("Errors").on_error(e,None) + except Exception as err: + self.bot.dispatch("error", err) async def setup(bot): diff --git a/fcts/xp.py b/fcts/xp.py index bbfbc0c7..c9f70094 100644 --- a/fcts/xp.py +++ b/fcts/xp.py @@ -44,15 +44,16 @@ def __init__(self, bot: Zbot): self.types = ['global','mee6-like','local'] try: verdana_name = 'Verdana.ttf' - xp_font = ImageFont.truetype(verdana_name, 24) except OSError: verdana_name = 'Veranda.ttf' - xp_font = ImageFont.truetype(verdana_name, 24) - self.fonts = {'xp_fnt': xp_font, - 'NIVEAU_fnt': ImageFont.truetype(verdana_name, 42), - 'levels_fnt': ImageFont.truetype(verdana_name, 65), - 'rank_fnt': ImageFont.truetype(verdana_name,29), - 'RANK_fnt': ImageFont.truetype(verdana_name,23)} + self.fonts = { + 'xp_fnt': ImageFont.truetype(verdana_name, 24), + 'NIVEAU_fnt': ImageFont.truetype(verdana_name, 42), + 'levels_fnt': ImageFont.truetype(verdana_name, 65), + 'rank_fnt': ImageFont.truetype(verdana_name, 29), + 'RANK_fnt': ImageFont.truetype(verdana_name, 23), + 'name_fnt': ImageFont.truetype('Roboto-Medium.ttf', 40), + } @commands.Cog.listener() async def on_ready(self): @@ -68,7 +69,7 @@ async def get_lvlup_chan(self, msg: discord.Message): if value == "any": return msg.channel try: - chan = msg.guild.get_channel(int(value)) + chan = msg.guild.get_channel_or_thread(int(value)) return chan except discord.errors.NotFound: return None @@ -291,9 +292,9 @@ async def give_rr(self, member: discord.Member, level: int, rr_list: list, remov if not self.bot.beta: await member.add_roles(r,reason="Role reward (lvl {})".format(role['level'])) c += 1 - except Exception as e: + except Exception as err: if self.bot.beta: - await self.bot.get_cog('Errors').on_error(e,None) + self.bot.dispatch("error", err) if not remove: return c for role in [x for x in rr_list if x['level']>level and x['role'] in has_roles]: @@ -304,9 +305,9 @@ async def give_rr(self, member: discord.Member, level: int, rr_list: list, remov if not self.bot.beta: await member.remove_roles(r,reason="Role reward (lvl {})".format(role['level'])) c += 1 - except Exception as e: + except Exception as err: if self.bot.beta: - await self.bot.get_cog('Errors').on_error(e,None) + self.bot.dispatch("error", err) return c async def reload_sus(self): @@ -373,8 +374,8 @@ async def bdd_set_xp(self, userID: int, points: int, Type: str='add', guild: int cnx.commit() cursor.close() return True - except Exception as e: - await self.bot.get_cog('Errors').on_error(e,None) + except Exception as err: + self.bot.dispatch("error", err) return False async def bdd_get_xp(self, userID: int, guild: int): @@ -405,8 +406,8 @@ async def bdd_get_xp(self, userID: int, guild: int): self.cache[g][userID] = [round(time.time())-60,liste[0]['xp']] cursor.close() return liste - except Exception as e: - await self.bot.get_cog('Errors').on_error(e,None) + except Exception as err: + self.bot.dispatch("error", err) async def bdd_get_nber(self, guild: int=None): """Get the number of ranked users""" @@ -431,8 +432,8 @@ async def bdd_get_nber(self, guild: int=None): if liste is not None and len(liste)==1: return liste[0][0] return 0 - except Exception as e: - await self.bot.get_cog('Errors').on_error(e,None) + except Exception as err: + self.bot.dispatch("error", err) async def bdd_load_cache(self, guild: int): try: @@ -469,8 +470,8 @@ async def bdd_load_cache(self, guild: int): self.cache[guild][l['userID']] = [round(time.time())-60, int(l['xp'])] cursor.close() return - except Exception as e: - await self.bot.get_cog('Errors').on_error(e,None) + except Exception as err: + self.bot.dispatch("error", err) async def bdd_get_top(self, top: int=None, guild: discord.Guild=None): try: @@ -508,7 +509,7 @@ async def bdd_get_top(self, top: int=None, guild: discord.Guild=None): cursor.close() return liste except Exception as err: - await self.bot.get_cog('Errors').on_error(err,None) + self.bot.dispatch("error", err) async def bdd_get_rank(self, userID: int, guild: discord.Guild=None): """Get the rank of a user""" @@ -525,10 +526,10 @@ async def bdd_get_rank(self, userID: int, guild: discord.Guild=None): cursor = cnx.cursor(dictionary = True) try: cursor.execute(query) - except mysql.connector.errors.ProgrammingError as e: - if e.errno == 1146: + except mysql.connector.errors.ProgrammingError as err: + if err.errno == 1146: return {"rank":0, "xp":0} - raise e + raise err userdata = dict() i = 0 users = list() @@ -544,8 +545,8 @@ async def bdd_get_rank(self, userID: int, guild: discord.Guild=None): break cursor.close() return userdata - except Exception as e: - await self.bot.get_cog('Errors').on_error(e,None) + except Exception as err: + self.bot.dispatch("error", err) async def bdd_total_xp(self): """Get the total number of earned xp""" @@ -568,8 +569,8 @@ async def bdd_total_xp(self): # result += round(res[0][0]) # cursor.close() return result - except Exception as e: - await self.bot.get_cog('Errors').on_error(e,None) + except Exception as err: + self.bot.dispatch("error", err) async def get_raw_image(self, url:str): @@ -586,7 +587,7 @@ def calc_pos(self, text:str, font, x: int, y: int, align: str='center'): async def create_card(self, user: discord.User, style, xp, used_system:int, rank=[1,0], txt=['NIVEAU','RANG'], force_static=False, levels_info=None): """Crée la carte d'xp pour un utilisateur""" - card = Image.open("../cards/model/{}.png".format(style)) + card = Image.open("./assets/card-models/{}.png".format(style)) bar_colors = await self.get_xp_bar_color(user.id) if levels_info is None: levels_info = await self.calc_level(xp,used_system) @@ -594,14 +595,12 @@ async def create_card(self, user: discord.User, style, xp, used_system:int, rank if style in {'blurple21', 'blurple22'}: colors = {'name':(240, 240, 255),'xp':(235, 235, 255),'NIVEAU':(245, 245, 255),'rank':(255, 255, 255),'bar':(250, 250, 255), 'bar_background': (27, 29, 31)} - name_fnt = ImageFont.truetype('Roboto-Medium.ttf', 40) - if not user.display_avatar.is_animated() or force_static: pfp = await self.get_raw_image(user.display_avatar.replace(format="png", size=256)) - img = await self.bot.loop.run_in_executor(None,self.add_overlay,pfp.resize(size=(282,282)),user,card,xp,rank,txt,colors,levels_info,name_fnt) - img.save('../cards/global/{}-{}-{}.png'.format(user.id,xp,rank[0])) + img = await self.bot.loop.run_in_executor(None,self.add_overlay,pfp.resize(size=(282,282)),user,card,xp,rank,txt,colors,levels_info) + img.save('./assets/cards/{}-{}-{}.png'.format(user.id,xp,rank[0])) card.close() - return discord.File('../cards/global/{}-{}-{}.png'.format(user.id,xp,rank[0])) + return discord.File('./assets/cards/{}-{}-{}.png'.format(user.id,xp,rank[0])) else: async with aiohttp.ClientSession() as cs: @@ -614,7 +613,7 @@ async def create_card(self, user: discord.User, style, xp, used_system:int, rank frames = [frame.copy() for frame in ImageSequence.Iterator(pfp)] for frame in frames: frame = frame.convert(mode='RGBA') - img = await self.bot.loop.run_in_executor(None,self.add_overlay,frame.resize(size=(282,282)),user,card.copy(),xp,rank,txt,colors,levels_info,name_fnt) + img = await self.bot.loop.run_in_executor(None,self.add_overlay,frame.resize(size=(282,282)),user,card.copy(),xp,rank,txt,colors,levels_info) img = ImageEnhance.Contrast(img).enhance(1.5).resize((800,265)) images.append(img) duration.append(pfp.info['duration']) @@ -623,13 +622,13 @@ async def create_card(self, user: discord.User, style, xp, used_system:int, rank # image_file_object = BytesIO() gif = images[0] - filename = '../cards/global/{}-{}-{}.gif'.format(user.id,xp,rank[0]) + filename = './assets/cards/{}-{}-{}.gif'.format(user.id,xp,rank[0]) gif.save(filename, format='gif', save_all=True, append_images=images[1:], loop=0, duration=duration, subrectangles=True) # image_file_object.seek(0) # return discord.File(fp=image_file_object, filename='card.gif') - return discord.File('../cards/global/{}-{}-{}.gif'.format(user.id,xp,rank[0])) - # imageio.mimwrite('../cards/global/{}-{}-{}.gif'.format(user.id,xp,rank[0]), images, format="GIF-PIL", duration=duration, subrectangles=True) - # return discord.File('../cards/global/{}-{}-{}.gif'.format(user.id,xp,rank[0])) + return discord.File('./assets/cards/{}-{}-{}.gif'.format(user.id,xp,rank[0])) + # imageio.mimwrite('./assets/cards/{}-{}-{}.gif'.format(user.id,xp,rank[0]), images, format="GIF-PIL", duration=duration, subrectangles=True) + # return discord.File('./assets/cards/{}-{}-{}.gif'.format(user.id,xp,rank[0])) def compress(self, original_file, max_size, scale: float): assert(0.0 < scale < 1.0) @@ -647,7 +646,7 @@ def compress(self, original_file, max_size, scale: float): file_bytes.seek(0, 0) return file_bytes - def add_overlay(self, pfp, user: discord.User, img, xp: int, rank: list, txt: list, colors, levels_info, name_fnt): + def add_overlay(self, pfp, user: discord.User, img, xp: int, rank: list, txt: list, colors, levels_info): #img = Image.new('RGBA', (card.width, card.height), color = (250,250,250,0)) #img.paste(pfp, (20, 29)) #img.paste(card, (0, 0), card) @@ -666,6 +665,7 @@ def add_overlay(self, pfp, user: discord.User, img, xp: int, rank: list, txt: li levels_fnt = self.fonts['levels_fnt'] rank_fnt = self.fonts['rank_fnt'] RANK_fnt = self.fonts['RANK_fnt'] + name_fnt = self.fonts['name_fnt'] img = self.add_xp_bar(img,xp-levels_info[2],levels_info[1]-levels_info[2],colors['bar'], colors['bar_background']) d = ImageDraw.Draw(img) @@ -750,18 +750,26 @@ async def rank(self, ctx: MyContext, *, user: args.user=None): rank = "?" if isinstance(rank, float): rank = int(rank) + # If we can send the rank card if ctx.guild is None or ctx.channel.permissions_for(ctx.guild.me).attach_files: - await self.send_card(ctx,user,xp,rank,ranks_nb,xp_used_type,levels_info) - elif ctx.can_send_embed: - await self.send_embed(ctx,user,xp,rank,ranks_nb,levels_info,xp_used_type) - else: - await self.send_txt(ctx,user,xp,rank,ranks_nb,levels_info,xp_used_type) - except Exception as e: - await self.bot.get_cog('Errors').on_command_error(ctx,e) + try: + await self.send_card(ctx, user, xp, rank, ranks_nb, xp_used_type, levels_info) + return + except Exception as err: # pylint: disable=broad-except + # log the error and fall back to embed/text + self.bot.dispatch("error", err, ctx) + # if we can send embeds + if ctx.can_send_embed: + await self.send_embed(ctx, user, xp, rank, ranks_nb, levels_info, xp_used_type) + return + # fall back to raw text + await self.send_txt(ctx, user, xp, rank, ranks_nb, levels_info, xp_used_type) + except Exception as err: + self.bot.dispatch("command_error", ctx, err) async def send_card(self, ctx: MyContext, user: discord.User, xp, rank, ranks_nb, used_system, levels_info=None): try: - myfile = discord.File('../cards/global/{}-{}-{}.{}'.format(user.id,xp,rank,'gif' if user.display_avatar.is_animated() else 'png')) + myfile = discord.File('./assets/cards/{}-{}-{}.{}'.format(user.id,xp,rank,'gif' if user.display_avatar.is_animated() else 'png')) except FileNotFoundError: style = await self.bot.get_cog('Utilities').get_xp_style(user) txts = [await self.bot._(ctx.channel, "xp.card-level"), await self.bot._(ctx.channel, "xp.card-rank")] @@ -776,8 +784,8 @@ async def send_card(self, ctx: MyContext, user: discord.User, xp, rank, ranks_nb if UsersCog := self.bot.get_cog("Users"): try: await UsersCog.used_rank(user.id) - except Exception as e: - await self.bot.get_cog("Errors").on_error(e, ctx) + except Exception as err: + self.bot.dispatch("error", err, ctx) if statsCog := self.bot.get_cog("BotStats"): statsCog.xp_cards["generated"] += 1 try: @@ -790,22 +798,22 @@ async def send_card(self, ctx: MyContext, user: discord.User, xp, rank, ranks_nb async def send_embed(self, ctx: MyContext, user: discord.User, xp, rank, ranks_nb, levels_info, used_system): txts = [await self.bot._(ctx.channel, "xp.card-level"), await self.bot._(ctx.channel, "xp.card-rank")] if levels_info is None: - levels_info = await self.calc_level(xp,used_system) + levels_info = await self.calc_level(xp, used_system) emb = discord.Embed(color=self.embed_color) emb.set_author(name=user, icon_url=user.display_avatar) - emb.add_field(name='XP', value="{}/{}".format(xp,levels_info[1])) - emb.add_field(name=txts[0], value=levels_info[0]) - emb.add_field(name=txts[1], value="{}/{}".format(rank,ranks_nb)) + emb.add_field(name='XP', value="{}/{}".format(xp, levels_info[1])) + emb.add_field(name=txts[0].title(), value=levels_info[0]) + emb.add_field(name=txts[1].title(), value="{}/{}".format(rank, ranks_nb)) await ctx.send(embed=emb) async def send_txt(self, ctx: MyContext, user: discord.User, xp, rank, ranks_nb, levels_info, used_system): txts = [await self.bot._(ctx.channel, "xp.card-level"), await self.bot._(ctx.channel, "xp.card-rank")] if levels_info is None: - levels_info = await self.calc_level(xp,used_system) + levels_info = await self.calc_level(xp, used_system) msg = """__**{}**__ **XP** {}/{} **{}** {} -**{}** {}/{}""".format(user.name,xp,levels_info[1],txts[0],levels_info[0],txts[1],rank,ranks_nb) +**{}** {}/{}""".format(user.name, xp, levels_info[1], txts[0].title(), levels_info[0], txts[1].title(), rank, ranks_nb) await ctx.send(msg) def convert_average(self, nbr: int) -> str: @@ -838,10 +846,10 @@ async def create_top_main(self, ranks, nbr, page, ctx: MyContext, used_system): txt.append('{} • **{} |** `lvl {}` **|** `xp {}`'.format(i,"__"+user_name+"__" if user==ctx.author else user_name,l[0],xp)) return txt,i - @commands.command(name='top') + @commands.command(name="top") @commands.bot_has_permissions(send_messages=True) @commands.cooldown(5,60,commands.BucketType.user) - async def top(self, ctx: MyContext, page: typing.Optional[int]=1, Type: args.LeaderboardType='global'): + async def top(self, ctx: MyContext, page: typing.Optional[int]=1, scope: args.LeaderboardType='global'): """Get the list of the highest levels Each page has 20 users @@ -860,13 +868,13 @@ async def top(self, ctx: MyContext, page: typing.Optional[int]=1, Type: args.Lea xp_system_used = 0 xp_system_used = 0 if xp_system_used is None else xp_system_used if xp_system_used == 0: - if Type == 'global': + if scope == 'global': if len(self.cache["global"]) == 0: await self.bdd_load_cache(-1) ranks = sorted([{'userID':key, 'xp':value[1]} for key,value in self.cache['global'].items()], key=lambda x:x['xp'], reverse=True) max_page = ceil(len(self.cache['global'])/20) - elif Type == 'guild': - ranks = await self.bdd_get_top(10000,guild=ctx.guild) + elif scope == 'guild': + ranks = await self.bdd_get_top(10000, guild=ctx.guild) max_page = ceil(len(ranks)/20) else: #ranks = await self.bdd_get_top(20*page,guild=ctx.guild) @@ -876,6 +884,8 @@ async def top(self, ctx: MyContext, page: typing.Optional[int]=1, Type: args.Lea max_page = ceil(len(ranks)/20) if page < 1: return await ctx.send(await self.bot._(ctx.channel, "xp.low-page")) + elif max_page == 0: + return await ctx.send(await self.bot._(ctx.channel, "xp.no-rank")) elif page > max_page: return await ctx.send(await self.bot._(ctx.channel, "xp.high-page")) ranks = ranks[(page-1)*20:] @@ -888,7 +898,7 @@ async def top(self, ctx: MyContext, page: typing.Optional[int]=1, Type: args.Lea await asyncio.sleep(0.2) f_name = await self.bot._(ctx.channel, "xp.top-name", min=(page-1)*20+1, max=i, page=page, total=max_page) # author - rank = await self.bdd_get_rank(ctx.author.id,ctx.guild if (Type=='guild' or xp_system_used != 0) else None) + rank = await self.bdd_get_rank(ctx.author.id,ctx.guild if (scope=='guild' or xp_system_used != 0) else None) if len(rank) == 0: your_rank = {'name':"__"+await self.bot._(ctx.channel, "xp.top-your")+"__",'value':await self.bot._(ctx.guild, "xp.1-no-xp")} else: @@ -899,7 +909,7 @@ async def top(self, ctx: MyContext, page: typing.Optional[int]=1, Type: args.Lea your_rank = {'name':"__"+await self.bot._(ctx.channel, "xp.top-your")+"__", 'value':"**#{} |** `lvl {}` **|** `xp {}`".format(rk, lvl, xp)} del rk # title - if Type == 'guild' or xp_system_used != 0: + if scope == 'guild' or xp_system_used != 0: t = await self.bot._(ctx.channel, "xp.top-title-2") else: t = await self.bot._(ctx.channel, "xp.top-title-1") @@ -915,13 +925,13 @@ async def top(self, ctx: MyContext, page: typing.Optional[int]=1, Type: args.Lea async def clear_cards(self, all: bool=False): """Delete outdated rank cards""" - files = os.listdir('../cards/global') - done = list() - for f in sorted([f.split('-')+['../cards/global/'+f] for f in files], key=operator.itemgetter(1), reverse=True): + files = os.listdir('./assets/cards/') + done: set[str] = set() + for f in sorted([f.split('-')+['./assets/cards/'+f] for f in files], key=operator.itemgetter(1), reverse=True): if all or f[0] in done: os.remove(f[3]) else: - done.append(f[0]) + done.add(f[0]) @commands.command(name='set_xp', aliases=["setxp", "set-xp"]) @@ -942,9 +952,9 @@ async def set_xp(self, ctx: MyContext, user: args.user, xp: int): prev_xp = await self.get_xp(user, None if xp_used_type == 0 else ctx.guild.id) await self.bdd_set_xp(user.id, xp, Type='set', guild=ctx.guild.id) await ctx.send(await self.bot._(ctx.guild.id, "xp.change-xp-ok", user=str(user), xp=xp)) - except Exception as e: + except Exception as err: await ctx.send(await self.bot._(ctx.guild.id, "minecraft.serv-error")) - await self.bot.get_cog('Errors').on_error(e,ctx) + self.bot.dispatch("error", err, ctx) else: if ctx.guild.id not in self.cache.keys(): await self.bdd_load_cache(ctx.guild.id) @@ -991,7 +1001,7 @@ async def rr_main(self, ctx: MyContext): ..Doc server.html#roles-rewards""" if ctx.subcommand_passed is None: - await self.bot.get_cog('Help').help_command(ctx,['rr']) + await ctx.send_help(ctx.command) @rr_main.command(name="add") @commands.check(checks.has_manage_guild) @@ -1009,12 +1019,11 @@ async def rr_add(self, ctx: MyContext, level:int, *, role:discord.Role): if len([x for x in l if x['level']==level]) > 0: return await ctx.send(await self.bot._(ctx.guild.id, "xp.already-1-rr")) max_rr = await self.bot.get_config(ctx.guild.id,'rr_max_number') - max_rr = self.bot.get_cog("Servers").default_opt['rr_max_number'] if max_rr is None else max_rr if len(l) >= max_rr: return await ctx.send(await self.bot._(ctx.guild.id, "xp.too-many-rr", c=len(l))) await self.rr_add_role(ctx.guild.id,role.id,level) - except Exception as e: - await self.bot.get_cog('Errors').on_command_error(ctx,e) + except Exception as err: + self.bot.dispatch("command_error", ctx, err) else: await ctx.send(await self.bot._(ctx.guild.id, "xp.rr-added", role=role.name,level=level)) @@ -1026,12 +1035,11 @@ async def rr_list(self, ctx: MyContext): ..Doc server.html#roles-rewards""" try: l = await self.rr_list_role(ctx.guild.id) - except Exception as e: - await self.bot.get_cog('Errors').on_command_error(ctx,e) + except Exception as err: + self.bot.dispatch("command_error", ctx, err) else: des = '\n'.join(["• <@&{}> : lvl {}".format(x['role'], x['level']) for x in l]) max_rr = await self.bot.get_config(ctx.guild.id,'rr_max_number') - max_rr = self.bot.get_cog("Servers").default_opt['rr_max_number'] if max_rr is None else max_rr title = await self.bot._(ctx.guild.id,"xp.rr_list", c=len(l), max=max_rr) emb = discord.Embed(title=title, description=des, timestamp=ctx.message.created_at) emb.set_footer(text=ctx.author, icon_url=ctx.author.display_avatar) @@ -1051,8 +1059,8 @@ async def rr_remove(self, ctx: MyContext, level:int): if len(l) == 0: return await ctx.send(await self.bot._(ctx.guild.id, "xp.no-rr")) await self.rr_remove_role(l[0]['ID']) - except Exception as e: - await self.bot.get_cog('Errors').on_command_error(ctx,e) + except Exception as err: + self.bot.dispatch("command_error", ctx, err) else: await ctx.send(await self.bot._(ctx.guild.id, "xp.rr-removed", level=level)) @@ -1080,8 +1088,8 @@ async def rr_reload(self, ctx: MyContext): level = (await self.calc_level(member['xp'], used_system))[0] c += await self.give_rr(m, level, rr_list, remove=True) await ctx.send(await self.bot._(ctx.guild.id, "xp.rr-reload", role_count=c,member_count=ctx.guild.member_count)) - except Exception as e: - await self.bot.get_cog('Errors').on_command_error(ctx,e) + except Exception as err: + self.bot.dispatch("command_error", ctx, err) async def setup(bot: Zbot): diff --git a/lang/antiscam/en.json b/lang/antiscam/en.json index 75e7a5e0..420acafd 100644 --- a/lang/antiscam/en.json +++ b/lang/antiscam/en.json @@ -1,6 +1,7 @@ { "probabilities": "Probabilities:", + "report-empty": "This message has no content to report!", "report-successful": "We have well received your report, thank you for your contribution!", - "result": "Result:", + "result": "Antiscam detection result:", "url-score": "URL risk score: %{score}" } \ No newline at end of file diff --git a/lang/antiscam/fr.json b/lang/antiscam/fr.json index aa7eb9e0..d5749453 100644 --- a/lang/antiscam/fr.json +++ b/lang/antiscam/fr.json @@ -1,6 +1,7 @@ { "probabilities": "Probabilités :", + "report-empty": "Ce message n'a aucun contenu à signaler !", "report-successful": "Nous avons bien recu votre report, merci de votre contribution !", - "result": "Résultat :", + "result": "Résultat de la détection de scam :", "url-score": "Score de risque des URLs : %{score}" } \ No newline at end of file diff --git a/lang/antiscam/lolcat.json b/lang/antiscam/lolcat.json index 139dfb38..04587e10 100644 --- a/lang/antiscam/lolcat.json +++ b/lang/antiscam/lolcat.json @@ -1,6 +1,7 @@ { "probabilities": "Probabilities:", + "report-empty": "Uh, what dayawanna report??? I see no message there!", "report-successful": "Thanks sir, I gotcha!", - "result": "Scores:", + "result": "Anti-bad-ppl detecshun result:", "url-score": "How bad seems to be linkies: %{score}" } \ No newline at end of file diff --git a/lang/bot_events/lolcat.json b/lang/bot_events/lolcat.json index 10b98072..ae9b216e 100644 --- a/lang/bot_events/lolcat.json +++ b/lang/bot_events/lolcat.json @@ -1,7 +1,12 @@ { + "christmas": { + "already-collected": "Eh oh, u already got ur Christmas gift! Dont try 2 steal me!", + "collected": "Here's ur xmas 2022 card! U can use %{cmd} with \"christmas22\" 2 start usin' it right naw!!!" + }, "events-price-title": "Prizes 2 win", "nothing-desc": "There'r no events rn. Follow my bot news 2 know teh date of the next one!", "nothing-prices": "There's no thing 2 gain 4 this event :confused:", + "no-objectives": "Dis event has no goal, no life meaning, nothing. At this point I wonder who am I, really.\nBut maybe %{cmd} would be more useful.", "objectives": "Objectives", "points": "pts", "position-global": "Globol rank", diff --git a/lang/cases/en.json b/lang/cases/en.json index 464cb7b6..d80d2a7a 100644 --- a/lang/cases/en.json +++ b/lang/cases/en.json @@ -1,12 +1,13 @@ { - "cases-0": "%{nbr} cases found: (%{start}-%{end})", - "deleted": "The case #%{ID} has been deleted!", + "records_number": "%{nbr} records found", + "deleted": "The record #%{ID} has been deleted!", "display": { "date": "**Date:** %{data}", "duration": "**Duration:** %{data}", "guild": "**Server:** %{data}", "moderator": "**Moderator:** %{data}", "reason": "**Reason:** %{data}", + "title": "Record of %{user} (%{user_id})", "type": "**Type:** %{data}", "user": "**User:** %{data}" }, diff --git a/lang/cases/fr.json b/lang/cases/fr.json index 840d4734..239c3c8f 100644 --- a/lang/cases/fr.json +++ b/lang/cases/fr.json @@ -1,5 +1,5 @@ { - "cases-0": "%{nbr} casiers trouvés : (%{start}-%{end})", + "records_number": "%{nbr} casiers trouvés", "deleted": "Le casier n°%{ID} a bien été supprimé !", "display": { "date": "**Date :** %{data}", @@ -7,6 +7,7 @@ "guild": "**Serveur :** %{data}", "moderator": "**Moderateur :** %{data}", "reason": "**Reason :** %{data}", + "title": "Casier de %{user} (%{user_id})", "type": "**Type :** %{data}", "user": "**Membre :** %{data}" }, diff --git a/lang/cases/lolcat.json b/lang/cases/lolcat.json index 4120293d..9b1af824 100644 --- a/lang/cases/lolcat.json +++ b/lang/cases/lolcat.json @@ -1,12 +1,13 @@ { - "cases-0": "%{nbr} cases fund: (%{start}-%{end})", - "deleted": "The caze #%{ID} has byn deletd!", + "records_number": "%{nbr} things found:", + "deleted": "The record #%{ID} has byn deletd!", "display": { "date": "**When?** %{data}", "duration": "**Haw long?** %{data}", "guild": "**Where?** %{data}", "moderator": "**By who?** %{data}", "reason": "**Why?** %{data}", + "title": "Record ov %{user_id} (aka %{user})", "type": "**What?** %{data}", "user": "**Who?** %{data}" }, diff --git a/lang/errors/de.json b/lang/errors/de.json index 8f8cfebb..605b9de4 100644 --- a/lang/errors/de.json +++ b/lang/errors/de.json @@ -23,6 +23,7 @@ "quoteserror": "Ups, ein Anführungszeichen Fehler ist aufgetreten. Schau nochmal nach ob du die Anführungszeichen “ richtig benutzt hast, sodass beide Anführungszeichen später geschlossen sind", "rolenotfound": "Die Rolle `%{r}` konnte nicht gefunden werden", "toomanytxtchan": "Du hast zu viele Erreichbare Textkanäle", - "unknown": "Ups, ein Fehler ist aufgetreten 😕 Versuche es später nochmal oder kontaktiere den Support (command `about`).", + "unknown": "Ups, ein Fehler ist aufgetreten 😕 Versuche es später nochmal oder kontaktiere den Support (command %{about}).", + "unknown2": "Ein Fehler ist während dem antworten aufgetreten. Bitte versuche es später oder kontaktiere den Zbot Support (gebe den %{about} Befehl für den Server Link ein)", "usernotfound": "Der Nutzer `%{u}` konnte nicht gefunden werden :confused:" } \ No newline at end of file diff --git a/lang/errors/en.json b/lang/errors/en.json index 38db2c5d..0a759b63 100644 --- a/lang/errors/en.json +++ b/lang/errors/en.json @@ -34,7 +34,8 @@ "quoteserror": "Oops, a quotation mark error has occurred. Be sure to correctly use the quotes \", so that each open quote will be closed further on", "rolenotfound": "Unable to find the role `%{r}`", "toomanytxtchan": "You have too many accessible text channels", - "unknown": "Oops, an error occurred :confused: Try again later, or contact support (command `about`).", + "unknown": "Oops, an error occurred :confused: Try again later, or contact support (command %{about}).", + "unknown2": "An error occurred while processing your response. Please try again later, or contact bot support (enter the command %{about} for server link)", "usernotfound": "Unable to find the user `%{u}` :confused:", - "zbot-migration": "This server now use Axobot, the new identity of Zbot. If you need help with the migration, don't hesitate to ask us at <%{invite}>!" + "zbot-migration": "This server now use <@1048011651145797673>, the new identity of Zbot. If you need help with the migration, don't hesitate to ask us at <%{invite}>!" } \ No newline at end of file diff --git a/lang/errors/fi.json b/lang/errors/fi.json index 909544c7..4b6c7f48 100644 --- a/lang/errors/fi.json +++ b/lang/errors/fi.json @@ -25,6 +25,7 @@ "quoteserror": "Ups, lainaus merkki virhe tapahtui. Varmista, että käytät oikeita lainaus merkkejä \", jotta jokainen avattu lainaus suljetaan myöhemmin", "rolenotfound": "On mahdotonta löytää rooli `%{r}`", "toomanytxtchan": "Sinulla on liikaa teksti kanavia", - "unknown": "Ups, tapahtui virhe 😕 Yritä uudelleen myöhemmin, tai ota yhteyttä tukeen (löytyy komennosta `about`).", + "unknown": "Ups, tapahtui virhe 😕 Yritä uudelleen myöhemmin, tai ota yhteyttä tukeen (löytyy komennosta %{about}).", + "unknown2": "Virhe tapahtui sinun vastauksen käsittelyssä. Yritä uudelleen myöhemmin, tai ota yhteyttä tukeen (kutsu linkin saat lähettämällä %{about} komennon)", "usernotfound": "On mahdotonta löytää käyttäjä `%{u}` :confused:" } \ No newline at end of file diff --git a/lang/errors/fr.json b/lang/errors/fr.json index d9925e42..ba52b1f9 100644 --- a/lang/errors/fr.json +++ b/lang/errors/fr.json @@ -34,7 +34,8 @@ "quoteserror": "Oups, une erreur de guillemets est survenue. Assurez-vous de bien utiliser les guillemets \", de manière à ce que chaque guillemet ouvert soit fermé plus loin", "rolenotfound": "Impossible de trouver le rôle `%{r}`", "toomanytxtchan": "Vous avez trop de salons textuels accessibles", - "unknown": "Oups, une erreur est survenue :confused: Réessayez plus tard, ou contactez le support (commande `about`).", + "unknown": "Oups, une erreur est survenue :confused: Réessayez plus tard, ou contactez le support (commande %{about}).", + "unknown2": "Une erreur s'est produite lors du traitement de votre réponse. Merci de réessayer plus tard, ou de contacter le support du bot (entrez la commande %{about} pour le lien du serveur)", "usernotfound": "Impossible de trouver l'utilisateur `%{u}` :confused:", - "zbot-migration": "Ce serveur utilise désormais Axobot, la nouvelle identité de Zbot. Si vous avez besoin d'aide pour la migration, n'hésitez pas à nous demander à <%{invite}> !" + "zbot-migration": "Ce serveur utilise désormais <@1048011651145797673>, la nouvelle identité de Zbot. Si vous avez besoin d'aide pour la migration, n'hésitez pas à nous demander à <%{invite}> !" } \ No newline at end of file diff --git a/lang/errors/fr2.json b/lang/errors/fr2.json index 400b19ad..e2dabf3f 100644 --- a/lang/errors/fr2.json +++ b/lang/errors/fr2.json @@ -25,6 +25,7 @@ "quoteserror": "Alors.\nUne erreur de guillemet, c'est ballot ! Mais le pire, c'est que ça vient d'arriver !\nFais bien attention à utiliser les guillemets \", comme ça chaque guillemet ouvert sera bien fermé un peu plus tard.", "rolenotfound": "Impossible de trouver ce p\\*\\*\\*\\* de rôle (`%{r}`) !", "toomanytxtchan": "Tu as trop de salons textuels accessibles...", - "unknown": "Wtf ?\nY'a eu une erreur 😠 😢 😕...\n\nRéessaie plus tard, ou contacte le support (commande `about`).", + "unknown": "Wtf ?\nY'a eu une erreur 😠 😢 😕...\n\nRéessaie plus tard, ou contacte le support (commande %{about}).", + "unknown2": "Une erreur s'est produite lors du traitement de ta réponse :blobconfused:. Merci de réessayer plus tard, ou de contacter le support du bot (entrez la commande %{about} pour le lien du serveur)", "usernotfound": "Impossible de trouver cet utilisateur `%{u}` :blobconfused:" } \ No newline at end of file diff --git a/lang/errors/hi.json b/lang/errors/hi.json index 3bff0b1b..0ccb2d05 100644 --- a/lang/errors/hi.json +++ b/lang/errors/hi.json @@ -31,6 +31,6 @@ "quoteserror": "ऊप्स, एक उद्धरण चिन्ह गलती हो गयी है। उद्धरण चिन्हों को सही तरह से उपयोग करें \", ताकि प्रत्येक उद्धरण चिन्ह आगे बंद हो जाए", "rolenotfound": "इस रोल को ढूंढने में अक्षम `%{r}`", "toomanytxtchan": "आपके पास बहुत अधिक टेक्स्ट चैनल की पहुँच है", - "unknown": "ऊप्स, एक कमी हो गयी :confused: कृपया दुबारा प्रयास करें, या सहायता प्राप्त करें (कमांड `about`).", + "unknown": "ऊप्स, एक कमी हो गयी :confused: कृपया दुबारा प्रयास करें, या सहायता प्राप्त करें (कमांड %{about}).", "usernotfound": "इस उपयोगकर्ता को ढूंढने में अक्षम `%{u}` :confused:" } diff --git a/lang/errors/lolcat.json b/lang/errors/lolcat.json index 56849e98..0963afac 100644 --- a/lang/errors/lolcat.json +++ b/lang/errors/lolcat.json @@ -34,6 +34,8 @@ "quoteserror": "Oops, u guys did smth wrong with quotes \" Make sur u using tehm correclty", "rolenotfound": "Unable 2 find ur role (`%{r}`)", "toomanytxtchan": "U have toooo many visibl text channels lmao. Why so much? useless/20. U should seriously think 'bout it dude. Pro-tip :smirk:", - "unknown": "Ups, somthin' wen't horribly wrong :fearful: pls contact zbot authorities immdiatly", - "usernotfound": "Unabled 2 find teh userZ `%{u}` :confused:" + "unknown": "Ups, somthin' wen't horribly wrong :fearful: pls contact zbot authorities immediatly (%{about}).", + "unknown2": "An fAtal erroR have occurred wHiLe proczzing ur respond. Plz trye again laterz, or contact suPPORt (use %{about} for teh server link)", + "usernotfound": "Unabled 2 find teh userZ `%{u}` :confused:", + "zbot-migration": "Dis serv naw use <@1048011651145797673>, teh new Zbot vershun. If ya ever need help with dis migrashin, ur welcome 2 ask us at <%{invite}>!" } \ No newline at end of file diff --git a/lang/fun/de.json b/lang/fun/de.json index d4c45ce6..20bba4ed 100644 --- a/lang/fun/de.json +++ b/lang/fun/de.json @@ -102,28 +102,21 @@ ], "tip-list": [ "Hast du schon gewusst, dass es mehrere Sprachen für diesen Bot gibt? Es gibt sogar aus Spaß lolcat", - "Pro-Tipp: Um die Sprache für Zbot zu ändern, benutze den `config` Befehl!", + "Pro-Tipp: Um die Sprache für Zbot zu ändern, benutze den %{config_cmd} Befehl!", "Pro-Tipp: Wenn du hilfe brauchst, benutze den `osekour` Befehl", "Hast du schon gewusst, dass die levelup Nachrichten zufällig sind? Um das zu tun hat Aragorn1202 eine Liste mit 60 von denen gemacht z.B. 'a bicorne', 'a cookie' or 'a banana'!", "Hast du schon gewusst, dass Zbots Profilbild mal ein... Creeper war?", "Hast du schon gewusst, dass Zbot von Talentierten Developern gemacht worden ist? Ein Admin, ein zweiter Admin, der nach Tierfutter und nach einer Katze benannt wurde, ein Ban Hammer und ein Pilz!", - "Pro-Tipp: Der `say ` Befehl ist sehr nützlich, um anonyme Nachrichten zu schicken... zumindest, wenn du den Befehl benutzen darfst.", - "Pro-Tipp: Der `discordlinks` Befehl wird dir alle nützlichen Links für Discord geben!", - "Hast du schon gewusst, dass alle Designs von Zbot von Adri#9223 gemacht worden sind? Auch die Liste von Emojis und der `bigtext` Befehl!", + "Pro-Tipp: Der %{say_cmd} Befehl ist sehr nützlich, um anonyme Nachrichten zu schicken... zumindest, wenn du den Befehl benutzen darfst.", + "Pro-Tipp: Der %{discordlinks_cmd} Befehl wird dir alle nützlichen Links für Discord geben!", + "Hast du schon gewusst, dass alle Designs von Zbot von Adri#9223 gemacht worden sind? Auch die Liste von Emojis und der %{bigtext_cmd} Befehl!", "Hast du schon gewusst, dass Zbot den Namen von Z_runner, seinem Entwickler bekommen hat?", - "Pro-Tipp: Zbot hat ein Discord, womit man die Bugs sehen und für das nächste Update voten kann! Nutze den `about` Befehl um die Einladung zu bekommen", + "Pro-Tipp: Zbot hat ein Discord, womit man die Bugs sehen und für das nächste Update voten kann! Nutze den %{about_cmd} Befehl um die Einladung zu bekommen", "Pro-Tipp: Der `prefix` Befehl erlaubt dir eine List, der nutzbaren Prefixe für den Server zu sehen", - "Hast du schon gewusst, dass du eine Beschreibung von deinem Server machen kannst, die in anderen Servern genutzt werden können. `description` ist die Option von dem `config` Befehl", + "Hast du schon gewusst, dass du eine Beschreibung von deinem Server machen kannst, die in anderen Servern genutzt werden können. `description` ist die Option von dem %{config_cmd} Befehl", "Hast du schon gewusst, dass bei manchen Events es möglich ist, limitierte xp cards zu bekommen. Vergiss es nicht, die Zbot News auf deinem Server zu aktivieren!", - "> tue das zum Überspringen\n*(hast du die Referenzen?)*", - "Pro-Tipp: Der `discordlinks` Befehl wird dir alle nützlich Links für Disocrd geben!", - "Hast du schon gewusst, dass alle Designs von Zbot von Adri#9223 gemacht worden sind, außerdem auch die große Liste mit allen Emojis und den `bigtext` Befehl!", "Hast du schon gewusst, dass Zbot seinen Namen von... seinem Creator Z_runner hat?", - "Pro-Tipp: Zbot hat ein Discord, womit du die aktuellen Bugs sehen und für die nächsten Updates stimmen kannst! Nutze den `about` Befehl um eine Einladung zu erstellen", - "Pro-Tipp: Der `prefix` Befehl erlaubt dir eine Liste mit allen aktuell nutzbaren Prefixen in diesem Server", - "Hast du schon gewusst, dass du deinem Server eine Beschreibung geben kannst, die in anderen Servern gesehen werden kann? nutze die `description` Option von dem `config` Befehl", - "Hast du schon gewusst, dass es möglich ist, bei speziellen Events, Punkte zu sammeln, um xp Cards freizuschalten. Vergiss nicht die Zbot news auf deinem Server zu aktivierten!", - "> Tue das, um zu überspringen\n*(hast du die Referenzen dafür?)*" + "Pro-Tipp: Der `prefix` Befehl erlaubt dir eine Liste mit allen aktuell nutzbaren Prefixen in diesem Server" ], "uninhabited-city": "Unbewohnte Stadt 😕", "vote-0": "Du kannst nicht mehr, als 20 Möglichkeiten oder eine negative Zahl angeben!" diff --git a/lang/fun/en.json b/lang/fun/en.json index 80472d40..b1e6d8e9 100644 --- a/lang/fun/en.json +++ b/lang/fun/en.json @@ -60,19 +60,6 @@ "{2}.exe *has stopped working*" ], "markdown": "__**Markdown Rules** on *Discord*__\n\n`*italics*` = *italics*\n`__underline__` = __underline__\n`**bold**` = **bold**\n`***bold italics***` = ***bold italics***\n`~~strikeout~~` = ~~strikeout~~\n`__*underline italics*__` = __*underline italics*__\n`__**underline bold**__` = __**underline bold**__\n`__***underline bold italics***__` = __***underline bold italics***__\n`||spoiler||` = ||spoiler||\n> quote = `> quote`\n\\`code\\` = `code`\n \\ to ignore\n\nFor code blocks, cf ", - "movie": { - "actors": "Actors", - "awards": "Awards", - "director": "Director", - "imdb-id": "IMDb ID", - "imdb-rating": "IMDb rating", - "no-description": "No description provided", - "no-rating": "Not rated", - "not-found": "Unable to find this movie", - "released": "Release date", - "runtime": "Runtime", - "writers": "Writers" - }, "nasa-none": "Oops, I'm unable to retrieve the information from NASA at the moment. Try again in a few minutes :confused:", "no-database": "As our database is offline, access to fun commands is restricted to people with permission \"Manage Server\"", "no-embed-perm": "I don't have permission to \"Embed links\" :confused:", @@ -116,28 +103,27 @@ ], "tip-list": [ "Did you know? There are several languages for the bot, including one very fun to test: lolcat", - "Pro-tip: to change the bot language, use the command `config`!", - "Pro-tip: you will find explanations of each command in the bot documentation, at https://zbot.rtfd.io Maybe even commands you didn't know about!", - "Did you know? The results of the `stats` command hide some servers, such as bot list servers, or internal ones", - "Did you know? This bot was originally designed for a server in a Minecraft community. That's where he made his name, and that's why he has commands on the theme of the game.", - "Did you know? The first version of the bot was written in February 2018, for personal use. At the time there were only two or three easy commands to make, like `clear` and `say`", - "Pro-tip: With the `say` command, you can use the emojis from any server where Zbot is, even animated emojis! Just give the emoji as if you were using it yourself", + "Pro-tip: to change the bot language, use the command %{config_cmd}!", + "Pro-tip: you will find explanations of each command in the bot documentation, at https://zbot.rtfd.io. Maybe even commands you didn't know about!", + "Did you know? The results of the %{stat_cmd} command hide some servers, such as bot list servers, or internal ones", + "Did you know? I was originally designed for a server in a Minecraft community. That is where I made my name, and that's why I have some Minecraft-related commands.", + "Did you know? The first version of the bot was written in February 2018, for personal use. At the time there were only two or three easy commands to make, like %{clear_cmd} and %{say_cmd}", + "Pro-tip: With the %{say_cmd} command, you can use the emojis from any server where I am, even animated emojis! Just give the emoji as if you were using it yourself", "Pro-tip: To use a custom emoji in the `react` command, just give its name. And it works with any emoji!", "Pro-tip: With the command `me `, you can make the bot say what you want, with your nickname in front of it! Like, for example, \"*Wumpus likes bananas*\".", "Pro-tip: The command `roll Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, PAN!` allows you to play Russian roulette! Be careful with that, though, okay?", "Pro-tip: If you need help, feel free to use the `osekour` command", "Did you know? Sometimes, the levelup message mentions a random object. To do this, Aragorn1202 had to create a list of 60 of them, including 'a bicorne', 'a cookie' or 'a banana'!", - "Did you know? In the past, Zbot's profile picture was... a creeper.", - "Did you know? The ZBot Staff is composed of a talented developer Admin, a second admin named after a kibble brand as well as a cat, a Ban hammer and a mushroom!", - "Pro-tip: The `say ` command is very useful for posting anonymous messages... at least when you have access to it.", - "Pro-tip: The `discordlinks` command will give you all the useful links related to discord!", - "Did you know? All designs related to the bot are made by Adri#9223, including the huge list of emojis used in the `bigtext` command!", - "Did you know? ZBot takes its name from.... its creator, Z_runner", - "Pro-tip: The bot has a Discord, where you can see the current bugs and vote for the next updates! Use the `about` command to get the invite", + "Did you know? In the past, my profile picture was... a creeper!", + "Did you know? The bot Staff is composed of a talented developer Admin, a second admin named after a kibble brand as well as a cat, a Ban hammer and a mushroom!", + "Pro-tip: The %{say_cmd} command is very useful for posting anonymous messages... at least when you have access to it.", + "Pro-tip: The %{discordlinks_cmd} command will give you all the useful links related to discord!", + "Did you know? All designs related to the bot are made by Adri#9223, including the huge list of emojis used in the %{bigtext_cmd} command!", + "Did you know? Until recently, my official name was 'Zbot', in reference to my creator, Z_runner", + "Pro-tip: The bot has a Discord, where you can see the current bugs and vote for the next updates! Use the %{about_cmd} command to get the invite", "Pro-tip: The `prefix` command allows you to have a list of the prefixes currently usable in the server", - "Did you know? You can give a description of your server that can be used in other servers, via the `description` option of the `config` command", - "Did you know? At some special events, it is possible to obtain collector xp cards. Don't forget to follow the news on the official server!", - "> this too shall pass\n*(do you have the reference?)*" + "Did you know? You can give a description of your server that can be used in other servers, via the `description` option of the %{config_cmd} command", + "Did you know? At some special events, it is possible to obtain collector xp cards. Don't forget to follow the news on the official server or with the %{event_cmd} command!" ], "uninhabited-city": "Uninhabited city :confused:", "vote-0": "You can't put more than 20 choices, and even less a negative number of choices!" diff --git a/lang/fun/es.json b/lang/fun/es.json index 80f35762..d6333970 100644 --- a/lang/fun/es.json +++ b/lang/fun/es.json @@ -53,12 +53,12 @@ ], "tip-list": [ "¿Sabías eso? Hay varios idiomas para el bot, incluyendo uno muy divertido: `lolcat`", - "Consejo profesional: para modificar el idioma del bot, use el comando `config` !", + "Consejo profesional: para modificar el idioma del bot, use el comando %{config_cmd} !", "Consejo profesional: Encontrarás explicaciones de cada comando en la documentación del bot, en https://zbot.rtfd.io; ¡Quizás incluso comandos que no conocías!", - "¿Sabías eso? Los resultados del comando `stats` ocultan algunos servidores, como los servidores de listas de robots, o los servidores internos.", + "¿Sabías eso? Los resultados del comando %{stats_cmd} ocultan algunos servidores, como los servidores de listas de robots, o los servidores internos.", "¿Sabías eso? El bot fue diseñado originalmente para un servidor en una comunidad de Minecraft. Ahí es donde se hizo famoso, y por eso que tiene comandos sobre el tema del juego.", - "¿Sabías eso? La primera versión del bot fue escrita en febrero de 2018, para uso privado. En ese momento sólo había dos o tres comandos fáciles de ejecutar, como `clear` y `say`.", - "Consejo profesional: Con el comando `say`, puedes usar los emojis desde todos los servidores donde Zbot esté, incluso los emojis animados. Sólo usa el emoji como si lo estuvieras usando tú mismo.", + "¿Sabías eso? La primera versión del bot fue escrita en febrero de 2018, para uso privado. En ese momento sólo había dos o tres comandos fáciles de ejecutar, como %{clear_cmd} y %{say_cmd}.", + "Consejo profesional: Con el comando %{say_cmd}, puedes usar los emojis desde todos los servidores donde Zbot esté, incluso los emojis animados. Sólo usa el emoji como si lo estuvieras usando tú mismo.", "Consejo profesional: Para usar un emoji personalizado en el comando `react`, sólo tienes que dar su nombre.", "Consejo profesional: Con el comando `me `, puedes hacer que el bot diga lo que quieras, con tu apodo delante! omo por ejemplo, \"*Wumpus le encantan los plátanos*\".", "Consejo profesional: El comando `roll Nada, Nada, Nada, Nada, Nada, Nada, Nada, PAN !` le permite jugar a la ruleta rusa! ¡Cuidado con eso!", @@ -66,13 +66,13 @@ "¿Sabías eso? A veces, el mensaje de elevación de nivel menciona un objeto aleatorio. Para ello, Aragorn1202 tuvo que crear una lista de 60 de ellos, incluyendo `a bicorne`, `una cookie` o `un plátano`.", "¿Sabías eso? En el pasado, la foto de perfil de Zbot era... una creeper.", "¿Sabías eso? El equipo de ZBot está compuesto por un talentoso desarrollador administrador, un segundo administrador llamado así por una marca de croquetas, así como un gato, un martillo ban y un hongo.", - "Consejo profesional: El comando `say ` es muy útil para publicar mensajes anónimos... por lo menos cuando tienes el derecho de usarlo.", - "Consejo profesional: ¡El comando `discordlinks` te dará todos los enlaces útiles relacionados con discord!", - "¿Sabías eso? Todos los diseños relacionados con el bot son realizados por Adri#9223, incluyendo la gran lista de emojis usados en el comando `bigtext`!", + "Consejo profesional: El comando %{say_cmd} es muy útil para publicar mensajes anónimos... por lo menos cuando tienes el derecho de usarlo.", + "Consejo profesional: ¡El comando %{discordlinks_cmd} te dará todos los enlaces útiles relacionados con discord!", + "¿Sabías eso? Todos los diseños relacionados con el bot son realizados por Adri#9223, incluyendo la gran lista de emojis usados en el comando %{bigtext_cmd}!", "¿Sabías eso? ZBot toma su nombre de... su creador, Z_runner", - "Consejo profesional: El bot tiene un Discord, donde puedes ver los errores actuales y votar por las próximas novedades! Usa el comando `about` para obtener la invitación.", + "Consejo profesional: El bot tiene un Discord, donde puedes ver los errores actuales y votar por las próximas novedades! Usa el comando %{about_cmd} para obtener la invitación.", "Consejo profesional: El comando `prefix` le permite tener una lista de los prefijos actualmente utilizables en el servidor.", - "¿Sabías eso? Puede dar una descripción de su servidor que puede ser usada en otros servidores, a través de la opción `description` del comando `config`.", + "¿Sabías eso? Puede dar una descripción de su servidor que puede ser usada en otros servidores, a través de la opción `description` del comando %{config_cmd}.", "¿Sabías eso? En algunos eventos especiales, es posible obtener tarjetas xp de colección. No olvides seguir las noticias en el servidor oficial!" ], "uninhabited-city": "Ciudad deshabitada :confused:", diff --git a/lang/fun/fi.json b/lang/fun/fi.json index 704d591c..ea43a5b4 100644 --- a/lang/fun/fi.json +++ b/lang/fun/fi.json @@ -101,31 +101,6 @@ "{0} oli jaettu Thanoksen kanssa", "Thanos päätti muuttaa {0} tuhkiin. Ihmiskunnan hyväksi ...." ], - "tip-list": [ - "Tiesitkö että, **Jees1#5825** käänsi tämän botin suomeksi?", - "Did you know that? There are several languages for the bot, including one very fun to test: lolcat", - "Pro-tip: to change the bot language, use the command `config` !", - "Pro-tip: you will find explanations of each command in the bot documentation, at https://zbot.rtfd.io Maybe even commands you didn't know about!", - "Did you know that? The results of the `stats` command hide some servers, such as bot list servers, or internal ones", - "Did you know that? This bot was originally designed for a server in a Minecraft community. That's where he made his name, and that's why he has commands on the theme of the game.", - "Did you know that? The first version of the bot was written in February 2018, for personal use. At the time there were only two or three easy commands to make, like `clear` and `say`", - "Pro-tip: With the `say` command, you can use the emojis from any server where Zbot is, even animated emojis! Just give the emoji as if you were using it yourself", - "Pro-tip: To use a custom emoji in the `react` command, just give its name. And it works with any emoji!", - "Pro-tip: With the command `me `, you can make the bot say what you want, with your nickname in front of it! Like, for example, \"*Wumpus likes bananas*\".", - "Pro-tip: The command `roll Nothing, Nothing, Nothing, Nothing, Nothing, PAN !` allows you to play Russian roulette! Be careful with that, though, okay?", - "Pro-tip: If you need help, feel free to use the `osekour` command", - "Did you know that? Sometimes, the levelup message mentions a random object. To do this, Aragorn1202 had to create a list of 60 of them, including 'a bicorne', 'a cookie' or 'a banana'!", - "Did you know that? In the past, Zbot's profile picture was... a creeper.", - "Did you know that? The ZBot Staff is composed of a talented developer Admin, a second admin named after a kibble brand as well as a cat, a Ban hammer and a mushroom!", - "Pro-tip: The `say ` command is very useful for posting anonymous messages... at least when you have access to it.", - "Pro-tip: The `discordlinks` command will give you all the useful links related to discord!", - "Did you know that? All designs related to the bot are made by Adri#9223, including the huge list of emojis used in the `bigtext` command!", - "Did you know that? ZBot takes its name from.... its creator, Z_runner", - "Pro-tip: The bot has a Discord, where you can see the current bugs and vote for the next updates! Use the `about` command to get the invite", - "Pro-tip: the `prefix` command allows you to have a list of the prefixes currently usable in the server", - "Tiesitkö että? Joissain tapahtumissa, on mahdollista saada keräys xp kortteja. Älä unohda seurata uutisia botin tuki palvelimessa!", - "> Tämä myös aikoo ohittaa\n*(onko sinulla viite siihen?)*" - ], "uninhabited-city": "Asumaton kaupunki :confused:", "vote-0": "Sinä voit laittaa enemmän kuin 20 vaihtoehtoa, ja myös vähemmän negatiivisia!" } \ No newline at end of file diff --git a/lang/fun/fr.json b/lang/fun/fr.json index 301bda7a..0bba9664 100644 --- a/lang/fun/fr.json +++ b/lang/fun/fr.json @@ -60,19 +60,6 @@ "{2}.exe *a cessé de fonctionner*" ], "markdown": "__**Règles du Markdown** sur *Discord*__\n\n`*italique*` = *italique*\n`__souligné__` = __souligné__\n`**gras**` = **gras**\n`***gras italique***` = ***gras italique***\n`~~barré~~` = ~~barré~~\n`__*souligné italique*__` = __*souligné italique*__\n`__**souligné gras**__` = __**souligné gras**__\n`__***souligné gras italique***__` = __***souligné gras italique***__\n`||spoiler||` = ||spoiler||\n> citation = `> citation`\n\\`code\\` = `code`\n\\\\ pour ignorer\n\nPour les blocs de code, cf ", - "movie": { - "actors": "Acteurs", - "awards": "Récompenses", - "director": "Réalisateur", - "imdb-id": "ID IMDb", - "imdb-rating": "Cote IMDb", - "no-description": "Aucune description disponible", - "no-rating": "Non coté", - "not-found": "Impossible de trouver ce film", - "released": "Date de sortie", - "runtime": "Durée", - "writers": "Scénaristes" - }, "nasa-none": "Oups, impossible de récupérer les informations de la NASA pour le moment. Réessayez dans quelques minutes :confused:", "no-database": "Notre base de donnée étant hors ligne, l'accès aux commandes fun est restreint aux personnes ayant la permission de Gérer le Serveur", "no-embed-perm": "Je ne possède pas la permission \"Intégrer des liens\" :confused:", @@ -116,28 +103,27 @@ ], "tip-list": [ "Le saviez-vous ? Il existe plusieurs langues pour le bot, dont une très amusante à tester : le lolcat", - "Pro-tip : pour changer la langue du bot, utilisez la commande `config` !", + "Pro-tip : pour changer la langue du bot, utilisez la commande %{config_cmd} !", "Pro-tip : vous trouverez les explications de chaque commande dans la documentation du bot, à l'adresse https://zbot.rtfd.io. Peut-être même des commandes que vous ne connaissiez pas !", - "Le saviez-vous ? Les résultats de la commande `stats` cachent certains serveurs, comme les serveurs de liste de bots, ou ceux internes", + "Le saviez-vous ? Les résultats de la commande %{stats_cmd} cachent certains serveurs, comme les serveurs de liste de bots, ou ceux internes", "Le saviez-vous ? Ce bot était au début conçu pour un serveur d'une communauté Minecraft. C'est de là qu'il s'est fait connaître, et c'est ce qui explique toutes ses commandes sur le thème du jeu", - "Le saviez-vous ? La première version du bot a été écrite en février 2018, pour un usage personnel. A l'époque il n'y avait que deux trois commandes faciles à faire, comme le `clear` et le `say`", - "Pro-tip : Avec la commande `say`, vous pouvez utiliser les émojis de n'importe quel serveur où est Zbot, même les émojis animés ! Il suffit de donner l'émoji comme si vous l'utilisez vous-même", + "Le saviez-vous ? La première version du bot a été écrite en février 2018, pour un usage personnel. A l'époque il n'y avait que deux trois commandes faciles à faire, comme le %{clear_cmd} et le %{say_cmd}", + "Pro-tip : Avec la commande %{say_cmd}, vous pouvez utiliser les émojis de n'importe quel serveur où je suis, même les émojis animés ! Il suffit de donner l'émoji comme si vous l'utilisez vous-même", "Pro-tip : Pour utiliser un émoji custom dans la commande `react`, il suffit de donner son nom. Et cela fonctionne avec n'importe quel émoji !", "Pro-tip : Avec la commande `me `, vous pouvez faire dire au bot ce que vous voulez, avec votre pseudo devant ! Comme par exemple \"*Wumpus aime les bananes*\"", "Pro-tip : La commande `roll Rien, Rien, Rien, Rien, Rien, PAN !` vous permet de jouer à la roulette russe ! Faites tout de même attention avec ça, d'accord ?", "Pro-tip : Si vous avez besoin d'aide, n'hésitez pas à utiliser la commande `osekour`", "Le saviez-vous ? Des fois, le message de levelup mentionne un objet aléatoire. Pour ce faire, Aragorn1202 a dû créer une liste de 60 d'entre eux, dont 'un bicorne', 'un cookie' ou 'une banane' !", - "Le saviez-vous ? Dans le passé, la photo de profil de Zbot était... un creeper.", - "Le saviez-vous ? Le Staff de ZBot est Composé d'un Admin développeur talentueux, d'un deuxième admin ayant comme nom une marque de croquette ainsi que d'un chat, un marteau de Ban et un champignon !", - "Pro-tip : La commande `say ` est très utile pour poster des messages anonymes... du moins quand vous y avez accès.", - "Pro-tip : La commande `discordlinks` vous donnera tous les liens utiles relatifs à discord !", - "Le saviez-vous ? Tous les designs relatifs au bot sont réalisés par Adri#9223, dont l'énorme liste d'émojis utilisés dans la commande `bigtext` !", - "Le saviez-vous ? ZBot tiens son nom... de son créateur, Z_runner", - "Pro-tip : Le bot possède un Discord, où vous pourrez voir les bugs actuels et voter pour les prochaines mises à jour ! Utilisez la commande `about` pour en obtenir le lien", + "Le saviez-vous ? Dans les premières versions, ma photo de profil était... un creeper.", + "Le saviez-vous ? Le Staff du bot est Composé d'un Admin développeur talentueux, d'un deuxième admin ayant comme nom une marque de croquette ainsi que d'un chat, un marteau de Ban et un champignon !", + "Pro-tip : La commande %{say_cmd} est très utile pour poster des messages anonymes... du moins quand vous y avez accès.", + "Pro-tip : La commande %{discordlinks_cmd} vous donnera tous les liens utiles relatifs à discord !", + "Le saviez-vous ? Tous les designs relatifs au bot sont réalisés par Adri#9223, dont l'énorme liste d'émojis utilisés dans la commande %{bigtext_cmd} !", + "Le saviez-vous ? Jusqu'à récemment, je m'appelais 'Zbot', en référence à créateur, Z_runner !", + "Pro-tip : Le bot possède un Discord, où vous pourrez voir les bugs actuels et voter pour les prochaines mises à jour ! Utilisez la commande %{about_cmd} pour en obtenir le lien", "Pro-tip : la commande `prefix` permet d'avoir la liste des préfixes actuellement utilisables dans le serveur", - "Le saviez-vous ? Vous pouvez donner une description de votre serveur qui pourra être utilisée dans d’autres serveurs, via l’option `description` de la commande `config`", - "Le saviez-vous ? Lors de certains événements spéciaux, il est possible d'obtenir des cartes d'xp collector. N'oubliez pas de suivre les news via le serveur officiel !", - "> Cela aussi passera\n*(vous avez la référence ?)*" + "Le saviez-vous ? Vous pouvez donner une description de votre serveur qui pourra être utilisée dans d’autres serveurs, via l’option `description` de la commande %{config_cmd}", + "Le saviez-vous ? Lors de certains événements spéciaux, il est possible d'obtenir des cartes d'xp collector. N'oubliez pas de suivre les news via le serveur officiel ou avec la commande %{event} !" ], "uninhabited-city": "Endroit inhabité", "vote-0": "Vous ne pouvez pas mettre plus de 20 choix, encore moins un nombre négatif de choix !" diff --git a/lang/fun/fr2.json b/lang/fun/fr2.json index a2f53234..4757c56a 100644 --- a/lang/fun/fr2.json +++ b/lang/fun/fr2.json @@ -103,12 +103,12 @@ ], "tip-list": [ "Le saviez-vous ? Il existe plusieurs langues pour le bot, dont une très amusante à tester : le lolcat", - "Pro-tip : pour changer la langue du bot, utilisez la commande `config` !", + "Pro-tip : pour changer la langue du bot, utilisez la commande %{config_cmd} !", "Pro-tip : vous trouverez les explications de chaque commande dans la documentation du bot, à l'adresse https://zbot.rtfd.io. Peut-être même des commandes que vous ne connaissiez pas !", - "Le saviez-vous ? Les résultats de la commande `stats` cachent certains serveurs, comme les serveurs de liste de bots, ou ceux internes", + "Le saviez-vous ? Les résultats de la commande %{stats_cmd} cachent certains serveurs, comme les serveurs de liste de bots, ou ceux internes", "Le saviez-vous ? Ce bot était au début conçu pour un serveur d'une communauté Minecraft. C'est de là qu'il s'est fait connaître, et c'est ce qui explique toutes ses commandes sur le thème du jeu", - "Le saviez-vous ? La première version du bot a été écrite en février 2018, pour un usage personnel. A l'époque il n'y avait que deux trois commandes faciles à faire, comme le `clear` et le `say`", - "Pro-tip : Avec la commande `say`, vous pouvez utiliser les émojis de n'importe quel serveur où est Zbot, même les émojis animés ! Il suffit de donner l'émoji comme si vous l'utilisez vous-même", + "Le saviez-vous ? La première version du bot a été écrite en février 2018, pour un usage personnel. A l'époque il n'y avait que deux trois commandes faciles à faire, comme le %{clear_cmd} et le %{say_cmd}", + "Pro-tip : Avec la commande %{say_cmd}, vous pouvez utiliser les émojis de n'importe quel serveur où est Zbot, même les émojis animés ! Il suffit de donner l'émoji comme si vous l'utilisez vous-même", "Pro-tip : Pour utiliser un émoji custom dans la commande `react`, il suffit de donner son nom. Et cela fonctionne avec n'importe quel émoji !", "Pro-tip : Avec la commande `me `, vous pouvez faire dire au bot ce que vous voulez, avec votre pseudo devant ! Comme par exemple \"*Wumpus aime les bananes*\"", "Pro-tip : La commande `roll Rien , Rien , Rien , Rien , Rien , PAN !` vous permet de jouer à la roulette russe ! Faites tout de même attention avec ça, d'accord ?", @@ -116,15 +116,14 @@ "Le saviez-vous ? Des fois, le message de levelup mentionne un objet aléatoire. Pour ce faire, Aragorn1202 a dû créer une liste de 60 d'entre eux, dont 'un bicorne', 'un cookie' ou 'une banane' !", "Le saviez-vous ? Dans le passé, la photo de profil de Zbot était... un creeper.", "Le saviez-vous ? Le Staff de ZBot est Composé d'un Admin développeur talentueux, d'un deuxième admin ayant comme nom une marque de croquette ainsi que d'un chat, un marteau de Ban et un champignon !", - "Pro-tip : La commande `say ` est très utile pour poster des messages anonymes... du moins quand vous y avez accès.", - "Pro-tip : La commande `discordlinks` vous donnera tous les liens utiles relatifs à discord !", - "Le saviez-vous ? Tous les designs relatifs au bot sont réalisés par Adri#9223, dont l'énorme liste d'émojis utilisés dans la commande `bigtext` !", + "Pro-tip : La commande %{say_cmd} est très utile pour poster des messages anonymes... du moins quand vous y avez accès.", + "Pro-tip : La commande %{discordlinks_cmd} vous donnera tous les liens utiles relatifs à discord !", + "Le saviez-vous ? Tous les designs relatifs au bot sont réalisés par Adri#9223, dont l'énorme liste d'émojis utilisés dans la commande %{bigtext_cmd} !", "Le saviez-vous ? ZBot tiens son nom... de son créateur, Z_runner", - "Pro-tip : Le bot possède un Discord, où vous pourrez voir les bugs actuels et voter pour les prochaines mises à jour ! Utilisez la commande `about` pour en obtenir le lien", + "Pro-tip : Le bot possède un Discord, où vous pourrez voir les bugs actuels et voter pour les prochaines mises à jour ! Utilisez la commande %{about_cmd} pour en obtenir le lien", "Pro-tip : la commande `prefix` permet d'avoir la liste des préfixes actuellement utilisables dans le serveur", - "Le saviez-vous ? Vous pouvez donner une description de votre serveur qui pourra être utilisée dans d’autres serveurs, via l’option `description` de la commande `config`", - "Le saviez-vous ? Lors de certains événements spéciaux, il est possible d'obtenir des cartes d'xp collector. N'oubliez pas de suivre les news via le serveur officiel !", - "> Cela aussi passera\n*(vous avez la référence ?)*" + "Le saviez-vous ? Vous pouvez donner une description de votre serveur qui pourra être utilisée dans d’autres serveurs, via l’option `description` de la commande %{config_cmd}", + "Le saviez-vous ? Lors de certains événements spéciaux, il est possible d'obtenir des cartes d'xp collector. N'oubliez pas de suivre les news via le serveur officiel !" ], "uninhabited-city": "Endroit inhabité", "vote-0": "Tu... Sais que tu peux pas mettre plus de 20 choix, et encore moins un nombre négatif de choix ?" diff --git a/lang/fun/hi.json b/lang/fun/hi.json index adae41af..90eb702d 100644 --- a/lang/fun/hi.json +++ b/lang/fun/hi.json @@ -32,100 +32,5 @@ "embed-invalid-channel": "इस चैनल में सन्देश नही भेज सकता", "embed-invalid-image": "छवि के रूप में डाला गया यूआरएल अमान्य है", "fun-list": "उपलब्ध मजेदार कमांड की सूची यह रही:", - "invalid-city": "अमान्य शहर :confused:", - "kills-list": [ - "अरे आप, आप मरने वाले हैं!", - "***BOUM!*** {1} {0} द्वारा बनाये गए जाल में फास गया!", - "Luckily, the ground has cushioned the fall of {1}!", - "{0} shouted \"Fus Roh Dah\" while {1} was next to a cliff...", - "No, you can't stop bullets with your hands {1} :shrug:", - "You have to be __in__ the elevator {1}, not __above__...", - "{1} stayed too close to the speakers during a heavy metal concert.", - "Staying within 10 meters of an atomic explosion wasn't a good idea {1}...", - "No! Double jumps are not possible {1}!", - "{1} imitated Icare... splash.", - "It's nice to have a portal gun {1}, but don't open portals above spades...", - "{1} died. Peace to their soul... :sneezing_face:", - "{0} killed {1}", - "{1} was shot by {0}", - "Bye {1}! :ghost:", - "{1} saw a flying anvil fall... on their head :head_bandage:", - "{1} commit suicide after {0} has cut their connection", - "Caution {1}! Fire burns :fire:", - "{1} fought zombies without shovel", - "{1} tried to hug a creeper", - "{1}, lava baths are hot, but lava burns...", - "{1} tried a rocket jump", - "You shouldn't listen to the pretty melody of the Lullaby, {1} :musical_note:", - "{2}.exe *has stopped working*" - ], - "markdown": "__**Markdown Rules** on *Discord*__\n\n`*italics*` = *italics*\n`__underline__` = __underline__\n`**bold**` = **bold**\n`***bold italics***` = ***bold italics***\n`~~strikeout~~` = ~~strikeout~~\n`__*underline italics*__` = __*underline italics*__\n`__**underline bold**__` = __**underline bold**__\n`__***underline bold italics***__` = __***underline bold italics***__\n`||spoiler||` = ||spoiler||\n> quote = `> quote`\n\\`code\\` = `code`\n \\ to ignore\n\nFor code blocks, cf ", - "nasa-none": "Oops, I'm unable to retrieve the information from NASA at the moment. Try again in a few minutes :confused:", - "no-database": "As our database is offline, access to fun commands is restricted to people with permission \"Manage Server\"", - "no-embed-perm": "I don't have permission to \"Embed links\" :confused:", - "no-emoji": "Unable to find this emoji!", - "no-fun": "Fun commands have been disabled on this server. To see their list, look at https://zbot.rtfd.io/en/latest/fun.html", - "no-reaction": "Unable to add reactions. Please check my permissions...", - "no-roll": "No choice found", - "no-say": "Unable to send any message in this channel", - "no-voicechan": "You must be in a vocal channel in order to use this command.", - "not-enough-roll": "There must be at least two distinct elements", - "osekour": [ - "Wait, I'm finishing watching my movie.", - "We're coming! But why don't you answer anymore? Don't fake death!", - "Yes, we know there's a fire, we don't need to come: we're having a barbecue at the fire station.", - "*Rescue is currently unavailable, please wait until the end of the break*", - "*This number does not exist. Please try again with another number.*", - "*Maintenance of the current line. Please try again in 430 hours.*", - "*Your mobile plan has expired. You can buy one for 86,25€*", - "Two more volumes of Lord of the Rings to finish reading, and I'm all yours!", - "Thank you for not disturbing us during the holidays", - "Sorry, there are more than 3 snowflakes: we're stuck in the garage", - "We'll have to wait until the end of our strike... Are you saying you don't know?! It's been two months since we started!" - ], - "piece-0": [ - "Tails!", - "Heads!" - ], - "piece-1": "Failed, it fell on the edge!", - "react-0": "Unable to find the corresponding message. You must enter the message ID in the first argument, and the emoji in the second :upside_down:\n Also check that I have permission to read the message history!", - "reminds-asked": "%{user}, you asked me %{duration} ago to remind you of this:", - "reminds-date": "Request date", - "reminds-link": "Original message", - "reminds-saved": "Got it! I'll remind you in %{duration}", - "reminds-title": "Reminder", - "reminds-too-long": "Impossible to set such a long duration", - "reminds-too-short": "You have entered an invalid duration", - "say-no-perm": "You do not have permission to send messages in the channel %{channel}", - "thanos": [ - "{0} was spared by Thanos", - "Thanos decided to reduce {0} to ashes. For the good of humanity...." - ], - "tip-list": [ - "Did you know? There are several languages for the bot, including one very fun to test: lolcat", - "Pro-tip: to change the bot language, use the command `config`!", - "Pro-tip: you will find explanations of each command in the bot documentation, at https://zbot.rtfd.io Maybe even commands you didn't know about!", - "Did you know? The results of the `stats` command hide some servers, such as bot list servers, or internal ones", - "Did you know? This bot was originally designed for a server in a Minecraft community. That's where he made his name, and that's why he has commands on the theme of the game.", - "Did you know? The first version of the bot was written in February 2018, for personal use. At the time there were only two or three easy commands to make, like `clear` and `say`", - "Pro-tip: With the `say` command, you can use the emojis from any server where Zbot is, even animated emojis! Just give the emoji as if you were using it yourself", - "Pro-tip: To use a custom emoji in the `react` command, just give its name. And it works with any emoji!", - "Pro-tip: With the command `me `, you can make the bot say what you want, with your nickname in front of it! Like, for example, \"*Wumpus likes bananas*\".", - "Pro-tip: The command `roll Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, PAN!` allows you to play Russian roulette! Be careful with that, though, okay?", - "Pro-tip: If you need help, feel free to use the `osekour` command", - "Did you know? Sometimes, the levelup message mentions a random object. To do this, Aragorn1202 had to create a list of 60 of them, including 'a bicorne', 'a cookie' or 'a banana'!", - "Did you know? In the past, Zbot's profile picture was... a creeper.", - "Did you know? The ZBot Staff is composed of a talented developer Admin, a second admin named after a kibble brand as well as a cat, a Ban hammer and a mushroom!", - "Pro-tip: The `say ` command is very useful for posting anonymous messages... at least when you have access to it.", - "Pro-tip: The `discordlinks` command will give you all the useful links related to discord!", - "Did you know? All designs related to the bot are made by Adri#9223, including the huge list of emojis used in the `bigtext` command!", - "Did you know? ZBot takes its name from.... its creator, Z_runner", - "Pro-tip: The bot has a Discord, where you can see the current bugs and vote for the next updates! Use the `about` command to get the invite", - "Pro-tip: The `prefix` command allows you to have a list of the prefixes currently usable in the server", - "Did you know? You can give a description of your server that can be used in other servers, via the `description` option of the `config` command", - "Did you know? At some special events, it is possible to obtain collector xp cards. Don't forget to follow the news on the official server!", - "> this too shall pass\n*(do you have the reference?)*" - ], - "uninhabited-city": "Uninhabited city :confused:", - "vote-0": "You can't put more than 20 choices, and even less a negative number of choices!" + "invalid-city": "अमान्य शहर :confused:" } diff --git a/lang/fun/lolcat.json b/lang/fun/lolcat.json index 2f2ce7eb..a56a0603 100644 --- a/lang/fun/lolcat.json +++ b/lang/fun/lolcat.json @@ -60,19 +60,6 @@ "{2}.exe *has stopeD wurkin*" ], "markdown": "__**Markdown Rules** on *Discord*__\n\n`*italics*` = *italics*\n`__underline__` = __underline__\n`**bold**` = **bold**\n`***bold italics***` = ***bold italics***\n`~~strikeout~~` = ~~strikeout~~\n`__*underline italics*__` = __*underline italics*__\n`__**underline bold**__` = __**underline bold**__\n`__***underline bold italics***__` = __***underline bold italics***__\n`||spoiler||` = ||spoiler||\n> quote = `> quote`\n\\`code\\` = `code`\n\\\\ to ignore\n \nFor code blocks, cf ", - "movie": { - "actors": "Players", - "awards": "Shiny awards", - "director": "Big boss", - "imdb-id": "IMDb ID", - "imdb-rating": "IMDb how-good-was-it?", - "no-description": "No text providd", - "no-rating": "No rate", - "not-found": "Oops no, I cannt find dis movie", - "released": "When-was-it-released", - "runtime": "How-long-is-it", - "writers": "Who wrote" - }, "nasa-none": "Oops, I can't find gud picts of bautiful stars for now, plz try later :crying_cat_face:", "no-database": "As our data ar offline, access to funz commandz iz restricted to guys with permishun \"Manage Server\"", "no-embed-perm": "I dont haz permishun 2 \"Embed links\" :confused:", @@ -116,28 +103,27 @@ ], "tip-list": [ "Did you know? There are several languages for the bot, including one very fun to test: lolcat", - "Pro-tip: to change the bot language, use the command `config` !", - "Pro-tip: you will find explanations of each command in the bot documentation, at https://zbot.rtfd.io Maybe even commands you didn't know about!", - "Did you know? The results of the `stats` command hide some servers, such as bot list servers, or internal ones", - "Did you know? This bot was originally designed for a server in a Minecraft community. That's where he made his name, and that's why he has commands on the theme of the game.", - "Did you know? The first version of the bot was written in February 2018, for personal use. At the time there were only two or three easy commands 2 make, like `clear` and `say`", - "Pro-tip: With the `say` command, you can use the emojis from any server where Zbot is, even animated emojis! Just give the emoji as if you were using it yourself", + "Pro-tip: to change the bot language, use %{config_cmd}!", + "Pro-tip: u'll find explanations ov each command in teh bot doc, at https://zbot.rtfd.io. Maybe even some super-cool tricks u didn't know be4!", + "Did u know? Teh results of %{stat_cmd} hide some servers, such as bot list servers, or super-private ones. Boring, isn't it?", + "Did u know? I was at first a minecraft-server bot. Dat's were I made my name, and why I have some minecraft-coolest commands!", + "Did u know? Teh first version of teh bot was written in february '18, for personal use. But 't was only a few useless commands like %{clear_cmd} or %{say_cmd}.", + "Pro-tip: U can use any emoji from any server where I am with the %{say_cmd} command, even animated ones!", "Pro-tip: To use a custom emoji in the `react` command, just give its name. And it works with any emoji!", "Pro-tip: With the command `me `, you can make the bot say what you want, with your nickname in front of it! Like, for example, \"*Wumpus likes bananas*\".", "Pro-tip: The command `roll Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, PAN !` allows you to play Russian roulette! Be careful with that, though, okay?", "Pro-tip: If you need help, feel free 2 use the `osekour` command", "Did you know? Sometimes, the levelup message mentions a random object. To do this, Aragorn1202 had to create a list of 60 of them, including 'a bicorne', 'a cookie' or 'a banana'!", - "Did you know? In the past, Zbot's profile picture was... a creeper.", - "Did you know? The ZBot Staff is composed of a talented developer Admin, a second admin named after a kibble brand as well as a cat, a Ban hammer and a mushroom!", - "Pro-tip: The `say ` command is very useful for posting anonymous messages... at least when you have access to it.", - "Pro-tip: The `discordlinks` command will give you all the useful links related to discord!", - "Did you know? All designs related 2 the bot are made by Adri#9223, including the huge list of emojis used in the `bigtext` command!", - "Did you know? ZBot takes its name from.... its creator, Z_runner", - "Pro-tip: The bot has a Discord, where you can see the current bugs and vote for the next updates! Use the `about` command to get the invite", + "Did u know? In the past, my avatar was... a creeper! Scary, isn't it?", + "Did u know? The bot staff is composed of a talented developer Admin, a second admin named after a kibble brand as well as a cat, a Ban hammer and a mushroom!", + "Pro-tip: Teh %{say_cmd} is very useful for... tbh idk. Probably fun stuff I can't imagine.", + "Pro-tip: Use %{discordlinks_cmd} for discord-related interesting links. Who would have guessed?", + "Did u know? All my design are made by the super famous artist Adri#9223, go check his profile!", + "Did u know? Until not-so-long-ago my name was 'Zbot', in ref 2 my creator Z_runner", + "Pro-tip: I've a super coll discord server, if you ever wanna follow my news and vote for the next updates. Check %{about_cmd} for the invite.", "Pro-tip: Teh `prefix` command allows you to have a list of the prefixes currently usable in the server", - "Did you know? U can give a description of your server that can be used in other servers, via the `description` option of the `config` command", - "Did you know? @ some special events, itz possible 2 obtain collector xp cards. Don't forget to follow the news on the officially cool server!", - "> this too shall pass\n*(do you have the reference?)*" + "Did u know? U can give a shiny description 2 ur server that may be usd in other servers, check %{config_cmd} with the `description` option!", + "Did u know? At some special event, u may be allowd 2 collect super shiny and rare xp cards! Follow the news on our official server or with teh %{event_cmd} command!" ], "uninhabited-city": "Tish place have 0 inhabitant :upside_down:", "vote-0": "U can't put moar than 20 choicez, an' even lesss negativ numbr of choicesz!" diff --git a/lang/fun/tr.json b/lang/fun/tr.json index 1c9fd802..6c8a4bed 100644 --- a/lang/fun/tr.json +++ b/lang/fun/tr.json @@ -58,28 +58,5 @@ "thanos": [ "{0} Thanos tarafından canı bağışlandı", "Thanos, {0}'i küllere dönüştürmeye karar verdi. İnsanlığın iyiliği için...." - ], - "tip-list": [ - "Biliyor musun? Bot için test edilecek çok eğlenceli bir dil de dahil olmak üzere birkaç dil vardır: lolcat", - "İpucu: bot dilini değiştirmek için, `config` komutunu kullanın!", - "İpucu: Yardıma ihtiyacınız varsa, `osekour` komutunu kullanmaktan çekinmeyin", - "Biliyor muydun? Bazen, seviye yükseltme mesajı rastgele bir nesneden bahseder. Bunu yapmak için Aragorn1202, 'bir bicorne', 'bir kurabiye' veya 'bir muz' dahil olmak üzere 60'lık bir liste oluşturmak zorunda kaldı!", - "Biliyor muydun? Eskiden Zbot'un profil resmi... bir creeperdı.", - "Biliyor muydun? ZBot Ekibi yetenekli bir geliştirici Yönetici, bir kibble markasının adını taşımasının yanı sıra bir kedi olan ikinci bir yönetici, bir Ban hammer ve bir mantardan oluşur!", - "İpucu: `say ` komutu anonim mesajlar göndermek için çok kullanışlıdır... en azından ona erişiminiz olduğunda.", - "İpucu: `discordlinks` komutu size discord ile ilgili tüm faydalı bağlantıları verecektir!", - "Biliyor muydun? `bigtext` komutunda kullanılan büyük emoji listesi de dahil olmak üzere bot ile ilgili tüm tasarımlar Adri#9223 tarafından yapılmıştır!", - "Özel bir püf nokta: Eğer yardıma ihtiyacın varsa, `osekour` komutunu kullanmaktan çekinme", - "Biliyor muydun? Bazen, seviye atlama mesajları rastgele bir obje gönderir. Bunu yapmak için Aragorn1202 'bir bicorne', 'bir kurabiye' veya 'bir muz' dahilinde 60 tanesinin listesini yapması gerekiyordu!", - "Biliyor muydun? Önceden Zbot'un profile fotoğrafı bir... creeperdı.", - "Biliyor muydun? ZBot yetkili ekibi yetenekli geliştirici yöneticisi, kibble markasının yanı sıra bir kedi, bir Yasaklama çekici ve bir mantarın karışımından oluşmaktadır!", - "Özel püf nokta: `say ` komutu gizli mesajlar yazmak için faydalıdır... en azından ona erişimin olduğu zaman.", - "Özel püf nokta: `discordlinks` komutu sana discord ile alakalı faydalı bağlantıların hepsini gönderir!", - "Biliyor muydun? `bigtext` komutunda kullanılan büyük emoji listesi de dahil olmak üzere, bot ile ilgili tüm tasarımlar, Adri#9223 tarafından yapılmıştır!", - "Biliyor muydun? ZBot adını.... yaratıcısı Z_runner'dan alıyor", - "İpucu: Bot, mevcut hataları görebileceğiniz ve bir sonraki güncellemelere oy verebileceğiniz bir Discord'a sahip! Daveti almak için `about` komutunu kullanın", - "İpucu: `prefix` komutu, şu anda sunucuda kullanılabilen prefixlerin bir listesine sahip olmanızı sağlar", - "Biliyor muydun? `config` komutunun `description` seçeneği ile sunucunuzun diğer sunucularda kullanılabilecek bir tanımını verebilirsiniz.", - "Biliyor muydun? Bazı özel etkinliklerde, koleksiyoncu xp kartları elde etmek mümkündür. Resmî sunucuda haberleri takip etmeyi unutmayın!" ] } \ No newline at end of file diff --git a/lang/help/en.json b/lang/help/en.json index 7cdda838..dd739a35 100644 --- a/lang/help/en.json +++ b/lang/help/en.json @@ -141,7 +141,11 @@ "The tic-tac-toe game must be enabled on this server" ] }, - "cmd-count": "%{nbr} commands\n`%{p}help %{cog}` to get the list", + "cmd-count": { + "zero": "No command", + "one": "1 command\n`%{p}help %{cog}` for more info", + "other": "%{count} commands\n`%{p}help %{cog}` to get the list" + }, "cmd-not-found": "There is no command named \"%{cmd}\"", "cog-empty-dm": "No command of this category is available in DM!", "description": "Description", diff --git a/lang/help/fr.json b/lang/help/fr.json index 77964dde..d11c5441 100644 --- a/lang/help/fr.json +++ b/lang/help/fr.json @@ -141,7 +141,11 @@ "Le jeu du morpion doit être activé sur ce serveur" ] }, - "cmd-count": "%{nbr} commandes\n`%{p}help %{cog}` pour en afficher la liste", + "cmd-count": { + "zero": "Aucune commande", + "one": "1 commande\n`%{p}help %{cog}` pour l'afficher", + "other": "%{count} commandes\n`%{p}help %{cog}` pour en afficher la liste" + }, "cmd-not-found": "Aucune commande nommée \"%{cmd}\"", "cog-empty-dm": "Aucune commande de ce module n'est disponible en MP !", "description": "Description", @@ -158,5 +162,5 @@ "not-enabled": ":warning: Cette commande est désactivée", "subcmd-not-found": "Cette commande ne possède aucune sous-commande nommée \"%{name}\"", "subcmds": "Sous-commandes", - "warning": "Avertissements" + "warning": "Prérequis" } \ No newline at end of file diff --git a/lang/help/lolcat.json b/lang/help/lolcat.json index ce157974..2015bd60 100644 --- a/lang/help/lolcat.json +++ b/lang/help/lolcat.json @@ -141,7 +141,11 @@ "Ewe, !ttt is disabled, use !config for dat" ] }, - "cmd-count": "%{nbr} itemz", + "cmd-count": { + "zero": "No cmd", + "one": "1 cmd\n`%{p}help%{cog}` 4 more info", + "other": "%{count} cmds\n`%{p}help %{cog}` 2 get teh full list" + }, "cmd-not-found": "Dere are no comand naymme \"%{cmd}\"", "cog-empty-dm": "Uhh no, dat box has no cmd for here :/ try in ur serv", "description": "Smol text", diff --git a/lang/info/en.json b/lang/info/en.json index 38be3893..65bb7105 100644 --- a/lang/info/en.json +++ b/lang/info/en.json @@ -11,7 +11,7 @@ "bitly_long": "Original URL: %{url}", "bitly_nobit": "This address is not a bit.ly link!", "bitly_short": "Shortened URL: %{url}", - "botinvite": "Here is a link to invite me to another server: %{url}\nDon't hesitate to test the `about` command for more useful links!", + "botinvite": "Here is a link to invite me to another server: %{url}\nDon't hesitate to test the %{about} command for more useful links!", "cant-see-channel": "Oops, looks like you don't have permission to view this channel. Ask a moderator!", "changelogs": { "index": "List of indexed versions", diff --git a/lang/info/fi.json b/lang/info/fi.json index 9afa7d0f..050a2bda 100644 --- a/lang/info/fi.json +++ b/lang/info/fi.json @@ -10,7 +10,7 @@ "bitly_long": "Alkuperäinen URL: %{url}", "bitly_nobit": "Tämä linkki ei ole bit.ly linkki!", "bitly_short": "Lyhennetty URL: %{url}", - "botinvite": "Tässä on linkki kutsuakseen minut toiseen palvelimeen: {url}\nÄlä unohda testata `about` komentoa hyödyllisille linkeille!", + "botinvite": "Tässä on linkki kutsuakseen minut toiseen palvelimeen: {url}\nÄlä unohda testata %{about} komentoa hyödyllisille linkeille!", "cant-see-channel": "Ups, näyttää siltä että sinulla ei ole oikeuksia nähdä tätä kanavaa! Kysy järjestyksenvalvojalta!", "changelogs": { "index": "Lista indeksoiduista versioista", diff --git a/lang/info/fr.json b/lang/info/fr.json index b787c98d..52a921e5 100644 --- a/lang/info/fr.json +++ b/lang/info/fr.json @@ -11,7 +11,7 @@ "bitly_long": "URL d'origine : %{url}", "bitly_nobit": "Cette adresse n'est pas un lien bit.ly !", "bitly_short": "URL raccourcie : %{url}", - "botinvite": "Voici un lien pour m'inviter dans un autre serveur : %{url}\nN'hésitez pas à tester la commande `about` pour plus de liens utiles !", + "botinvite": "Voici un lien pour m'inviter dans un autre serveur : %{url}\nN'hésitez pas à tester la commande %{about} pour plus de liens utiles !", "cant-see-channel": "Oups, il semble que vous n'avez pas la permission de voir ce salon. Demandez à un modérateur !", "changelogs": { "index": "Liste des versions indexées", diff --git a/lang/info/fr2.json b/lang/info/fr2.json index e2e103d5..cf4ce383 100644 --- a/lang/info/fr2.json +++ b/lang/info/fr2.json @@ -10,7 +10,7 @@ "bitly_long": "URL d'origine : %{url}", "bitly_nobit": "Cette adresse n'est pas un lien bit.ly !", "bitly_short": "URL raccourcie : %{url}\nCe sera moins chiant comme ça !", - "botinvite": "Voixi un lien pour m'inviter vers un autre serveur : %{url}\nN'hésite pas à essayer la commande `about` pour des liens plus utiles !", + "botinvite": "Voixi un lien pour m'inviter vers un autre serveur : %{url}\nN'hésite pas à essayer la commande %{about} pour des liens plus utiles !", "cant-see-channel": "Ups ! J'ai l'impression que tu n'as pas les perms pour voir ce salon. Demande à un modo !", "changelogs": { "index": "Liste des versions indexées", diff --git a/lang/info/lolcat.json b/lang/info/lolcat.json index 297cefd6..daf0cfb3 100644 --- a/lang/info/lolcat.json +++ b/lang/info/lolcat.json @@ -11,7 +11,7 @@ "bitly_long": "Slow link: %{url}", "bitly_nobit": "HAHA NOPE, dat's not a bit.ly link!", "bitly_short": "Quick linq: %{url}", - "botinvite": "Hers a link 2 invite me 2 'nother warm place: %{url}\nDon't hesitate to test my cool `about` cmd 4 + useful links!", + "botinvite": "Here's a link 2 invite me 2 somewhere else: %{url}\nAlso check %{about} 4 more useful links :)", "cant-see-channel": "Uhh nope, ur not allowed to see that!", "changelogs": { "index": "List of indexed versions", diff --git a/lang/misc/en.json b/lang/misc/en.json index 65a4e93d..f453fde4 100644 --- a/lang/misc/en.json +++ b/lang/misc/en.json @@ -28,9 +28,13 @@ "click_here": "Click here", "dnd": "do not disturb", "doc": "Documentation", + "done!": "Done!", "duration": "duration", "end": "end", - "example": "example", + "example": { + "one": "example", + "other": "examples" + }, "extreme": "extreme", "ghost": "Ghost", "high": "high", diff --git a/lang/misc/fr.json b/lang/misc/fr.json index 19759100..6bc0d8f8 100644 --- a/lang/misc/fr.json +++ b/lang/misc/fr.json @@ -28,9 +28,13 @@ "click_here": "Cliquez ici", "dnd": "ne pas déranger", "doc": "Documentation", + "done!": "Fait !", "duration": "durée", "end": "fin", - "example": "exemple", + "example": { + "one": "exemple", + "other": "exemples" + }, "extreme": "extrême", "ghost": "Fantôme", "high": "élevé", diff --git a/lang/misc/lolcat.json b/lang/misc/lolcat.json index 134f4327..95db4399 100644 --- a/lang/misc/lolcat.json +++ b/lang/misc/lolcat.json @@ -28,9 +28,13 @@ "click_here": "BLUE BUTTON TO CLICK", "dnd": "donot disturb me", "doc": "Doc", + "done!": "Did it! :proudcat:", "duration": "how long", "end": "end", - "example": "example", + "example": { + "one": "exmple", + "other": "exmples" + }, "extreme": "Xtrem", "ghost": "Goast", "high": "high", diff --git a/lang/moderation/en.json b/lang/moderation/en.json index 7d6fbd22..0a975410 100644 --- a/lang/moderation/en.json +++ b/lang/moderation/en.json @@ -46,6 +46,19 @@ "kick-dm": "You have just been kicked from the server %{guild} :confused:", "missing-manage-nick": "Oops, I'm missing the \"Manage nicknames\" permission!", "missing-user-perms": "Oops, you're missing permissions (either manage roles, manage server or manage messages)", + "mute-config": { + "confirm": "Are you sure you want to setup a \"muted\" role?\nThis action may break some advanced permissions in your server, by revoking the 'send messages' permission overwrite from some roles.", + "err": "Impossible to do that. Please check my permissions on the server and try again.", + "success": { + "other": "The role has been successfully created and configured! (%{count} channels could not be configured, due to lack of permissions)", + "zero": "The role has been successfully created and configured!" + }, + "success2": { + "other": "The role has been successfully configured! (%{count} channels could not be configured, due to lack of permissions)", + "zero": "The role has been successfully configured!" + }, + "tip": "*Tip: Using the %{mute} command without any muted role configured will use the Discord official timeout system, which might better suit your needs.*" + }, "mute": { "already-mute": "This member is already muted!", "already-unmute": "This member isn't muted!", @@ -54,19 +67,9 @@ "list-title-0": "List of muted members of the server \"%{guild}\"", "list-title-1": "List of 45 muted members of the server \"%{guild}\"", "list-title-2": "List of 60 muted members of the server \"%{guild}\"", - "mute-config-err": "Impossible to do that. Please check my permissions on the server and try again.", - "mute-config-success": { - "other": "The role has been successfully created and configured! (%{count} channels could not be configured, due to lack of permissions)", - "zero": "The role has been successfully created and configured!" - }, - "mute-config-success2": { - "other": "The role has been successfully configured! (%{count} channels could not be configured, due to lack of permissions)", - "zero": "The role has been successfully configured!" - }, "mute-high": "Oops, it seems that the `muted` role is too high for me to give it... Please fix this problem by placing my role higher than the `muted` role.", "no-mute": "Oops, it seems that the role `muted` does not exist :confused: Please create it and assign permissions manually.", - "no-mutes": "No member seems to be banned from here", - "role-created": "Successfully created `muted` role!\nYou can use the `%{p}mute-config` command for automatic configuration in your server.", + "no-mutes": "No member seems to be muted from here", "staff-mute": "You can't prevent another staff member from speaking", "too-high": "It seems that this member is too high for me to time out them out :thinking:" }, @@ -115,6 +118,7 @@ "one": "1 nickname has been edited", "zero": "No nickname have been edited" }, + "unhoist-too-many-members": "Oops, your server has too many members for me to do that. Please contact our support team for more info.", "unmute-chat": "The member %{user} (%{userid}) has been unmuted", "warn": { "cant-bot": "I can't warn a bot ^^", diff --git a/lang/moderation/fi.json b/lang/moderation/fi.json index 14ec4467..a4d73153 100644 --- a/lang/moderation/fi.json +++ b/lang/moderation/fi.json @@ -46,6 +46,17 @@ "kick-dm": "Sinut on juuri potkittu ulos palvelimelta %{guild} 😕", "missing-manage-nick": "Ups, minulta puuttuu \"Hallinnoi nimimerkkejä\" käyttöoikeus!", "missing-user-perms": "Ups, sinulta puuttuu käyttöoikeuksia (joko hallinnoi rooleja, hallinnoi palvelinta tai hallinnoi viestejä)", + "mute-config": { + "err": "Mahdotonta tehdä noin. Varmista onko minulla oikeat käyttöoikeudet ja yritä uudelleen.", + "success": { + "other": "Rooli on tehty ja konfiguroitu oikein! (%{count} kanavaa ei pystytty konfiguroimaan, oikeuksien puutteiden takia)", + "zero": "Rooli on tehty ja konfiguroitu oikein!" + }, + "success2": { + "other": "Rooli on konfiguroitu oikein! (%{count} kanavaa ei pystytty konfiguroimaan, oikeuksien puutteiden takia)", + "zero": "Rooli on konfiguroitu oikein!" + } + }, "mute": { "already-mute": "Tämä jäsen on jo mykistetty!", "already-unmute": "Tämä jäsen ei ole mykistetty!", @@ -53,19 +64,9 @@ "list-title-0": "Lista jäähyllä olevista jäsenistä palvelimella \"%{guild}\"", "list-title-1": "Lista 45 jäähyllä olevasta jäsenestä palvelimella \"%{guild}\"", "list-title-2": "Lista 60 jäähyllä olevasta jäsenestä palvelimella \"%{guild}\"", - "mute-config-err": "Mahdotonta tehdä noin. Varmista onko minulla oikeat käyttöoikeudet ja yritä uudelleen.", - "mute-config-success": { - "other": "Rooli on tehty ja konfiguroitu oikein! (%{count} kanavaa ei pystytty konfiguroimaan, oikeuksien puutteiden takia)", - "zero": "Rooli on tehty ja konfiguroitu oikein!" - }, - "mute-config-success2": { - "other": "Rooli on konfiguroitu oikein! (%{count} kanavaa ei pystytty konfiguroimaan, oikeuksien puutteiden takia)", - "zero": "Rooli on konfiguroitu oikein!" - }, "mute-high": "Ups, näyttää siltä että `mykistetty` rooli on liian korkealla että voisin antaa sen... Korjaa tämä sillä että laita minun roolini `Mykistetty` roolin yläpuolelle.", "no-mute": "Ups, näyttää siltä että `mykistetty` rooli ei ole olemassa 😕 Tede yksi ja anna käyttöoikeudet manuallisesti.", "no-mutes": "Kenelläkään ei näytä olevan porttikieltoa täällä", - "role-created": "`Muted` rooli tehty! Voit käyttää komentoa `%{p}mute-config` automaattiselle konfiguroinnille palvelimellasi.", "staff-mute": "Et voi mykistää toista työntekijää" }, "mute-chat": "Käyttäjä %{user} (%{userid}) on pistetty jäähylle", diff --git a/lang/moderation/fr.json b/lang/moderation/fr.json index c12c80d9..41f5c613 100644 --- a/lang/moderation/fr.json +++ b/lang/moderation/fr.json @@ -46,6 +46,19 @@ "kick-dm": "Vous venez d'être expulsé du serveur %{guild} :confused:", "missing-manage-nick": "Oups, il me manque la permission \"Gérer les pseudos\"", "missing-user-perms": "Oups, il vous manque des permissions (gérer les rôles, gérer le serveur ou gérer les messages)", + "mute-config": { + "confirm": "Êtes-vous sûr de vouloir configurer un rôle \"muted\" ?\nCette action peut casser certaines permissions avancées dans votre serveur, en révoquant la modification de la permission \"envoyer des messages\" de certains rôles.", + "err": "Impossible de faire cela. Veuillez vérifier mes permissions dans le serveur et réessayer.", + "success": { + "other": "Le rôle a été créé et configuré avec succès ! (%{count} salons n'ont pas pu être configurés, par manque de permissions)", + "zero": "Le rôle a été créé et configuré avec succès !" + }, + "success2": { + "other": "Le rôle a été configuré avec succès ! (%{count} salons n'ont pas pu être configurés, par manque de permissions)", + "zero": "Le rôle a été configuré avec succès !" + }, + "tip": "*Remarque : L'utilisation de la commande %{mute} sans qu'aucun rôle muet ne soit configuré utilisera le système officiel d'exclusion de Discord, qui pourrait mieux convenir à vos besoins.*" + }, "mute": { "already-mute": "Ce membre est déjà muet !", "already-unmute": "Ce membre n'est pas muet !", @@ -54,19 +67,9 @@ "list-title-0": "Liste des membres muets du serveur '%{guild}'", "list-title-1": "Liste des 45 premiers membres muets du serveur '%{guild}'", "list-title-2": "Liste des 60 premiers membres muets du serveur '%{guild}'", - "mute-config-err": "Impossible de faire cela. Veuillez vérifier mes permissions dans le serveur et réessayer.", - "mute-config-success": { - "other": "Le rôle a été créé et configuré avec succès ! (%{count} salons n'ont pas pu être configurés, par manque de permissions)", - "zero": "Le rôle a été créé et configuré avec succès !" - }, - "mute-config-success2": { - "other": "Le rôle a été configuré avec succès ! (%{count} salons n'ont pas pu être configurés, par manque de permissions)", - "zero": "Le rôle a été configuré avec succès !" - }, "mute-high": "Oups, il semble que le rôle `muted` soit trop haut pour que je puisse le donner... Veuillez fixer ce problème en plaçant mon rôle plus haut que le rôle `muted`.", "no-mute": "Oups, il semble que le rôle `muted` n'existe pas :confused: Veuillez le créer et lui attribuer les permissions manuellement.", "no-mutes": "Aucun membre ne semble être actuellement muet dans ce serveur", - "role-created": "Rôle `muted` créé avec succès !\nVous pouvez utiliser la commande `%{p}mute-config` pour une configuration automatique dans votre serveur.", "staff-mute": "Vous ne pouvez pas empêcher de parler un autre membre du staff", "too-high": "Il semble que ce membre soit trop haut pour que je puisse l'exclure :thinking:" }, @@ -115,6 +118,7 @@ "one": "1 surnom édité !", "zero": "Aucun surnom n'a été édité" }, + "unhoist-too-many-members": "Oups, votre serveur a trop de membres pour que je puisse faire cela. Veuillez contacter notre équipe de support pour plus d'informations.", "unmute-chat": "Le membre %{user} (%{userid}) peut à nouveau parler !", "warn": { "cant-bot": "Je ne peux pas avertir un bot ^^", diff --git a/lang/moderation/fr2.json b/lang/moderation/fr2.json index 9a42ee5f..c62fa691 100644 --- a/lang/moderation/fr2.json +++ b/lang/moderation/fr2.json @@ -45,6 +45,17 @@ "kick-dm": "Tu viens juste d'être kick de %{guild} 😕", "missing-manage-nick": "Oups, il me manque la permission \"Gérer les pseudos\"", "missing-user-perms": "Wolala 😬 !\nIl te manque des perms (style, manager les roles, le serveur, ou les messages)", + "mute-config": { + "err": "Euh...\nAlors, je peux pas faire ça.\n\nDonc va checker mes perms sur ce serveur, et ENSUITE tu réessaies.", + "success": { + "other": "Ce rôle a été créé et configuré avec succès ! (%{count} salons n'ont pas pu être configurés, par contre. J'avais pas les perms 😓)", + "zero": "Ce rôle a été créé et configuré avec succès !" + }, + "success2": { + "other": "Ce rôle a été configuré avec succès ! *(Bon, j'ai pas pu configurer %{count} salons, parce qu'il me manquait des perms 😓)*", + "zero": "Ce rôle a été configuré avec succès !" + } + }, "mute": { "already-mute": "Ce membre est déjà muet !", "already-unmute": "Ce membre n'est pas muet !", @@ -52,19 +63,9 @@ "list-title-0": "Liste des membres mute sur *%{guild}*", "list-title-1": "Liste de 45 membres mute sur *%{guild}*", "list-title-2": "Liste de 60 membres mute sur *%{guild}*", - "mute-config-err": "Euh...\nAlors, je peux pas faire ça.\n\nDonc va checker mes perms sur ce serveur, et ENSUITE tu réessaies.", - "mute-config-success": { - "other": "Ce rôle a été créé et configuré avec succès ! (%{count} salons n'ont pas pu être configurés, par contre. J'avais pas les perms 😓)", - "zero": "Ce rôle a été créé et configuré avec succès !" - }, - "mute-config-success2": { - "other": "Ce rôle a été configuré avec succès ! *(Bon, j'ai pas pu configurer %{count} salons, parce qu'il me manquait des perms 😓)*", - "zero": "Ce rôle a été configuré avec succès !" - }, "mute-high": "Oups, il semble que le rôle `muted` soit trop haut pour que je puisse le donner... Veuillez fixer ce problème en plaçant mon rôle plus haut que le rôle `muted`.", "no-mute": "Oups, il semble que le rôle `muted` n'existe pas :confused: Veuillez le créer et lui attribuer les permissions manuellement.", "no-mutes": "J'ai pas l'impression que quiconque soit mute, ici ¯\\_(ツ)_/¯", - "role-created": "Rôle `muted` configuré avec succès, chef !\nTu peux utiliser la commande `%{p}mute-config` pour une config automatique de ton serveur.", "staff-mute": "Vous ne pouvez pas empêcher de parler un autre membre du staff " }, "mute-chat": "%{user} (%{userid}) vient d'être mute.", diff --git a/lang/moderation/lolcat.json b/lang/moderation/lolcat.json index d102b0f0..5ac71934 100644 --- a/lang/moderation/lolcat.json +++ b/lang/moderation/lolcat.json @@ -46,6 +46,19 @@ "kick-dm": "U have just been kicked from the servr %{guild} :confused:", "missing-manage-nick": "Oops, I'm missing the cool \"Manage nicknames\" perms! :sad:", "missing-user-perms": "Oops, Ur not pawerful enoogh (either manage roles, manage server or manage messages)", + "mute-config": { + "confirm": "Are u sure u wanna setup teh \"muted\" role??\nI dont wanna be annoying, but uh, dat might break some things in ur server. Especially if u playd with teh \"send msg\" perms. Think twice or get bonk.", + "err": "Oops, cant do that. Plz check me perms on teh server 'nd try again.", + "success": { + "other": "Teh role have been successffully created & configured! (%{count} channels couldnotbeconfigured, cuz I lack of perms)", + "zero": "Teh role have been successffully created & configured!" + }, + "success2": { + "other": "The mouted role iz now correcty setup! (%{count} chats cannut be configured, cuz of a bruh lacking of perms)", + "zero": "The role iz now correcty setup!" + }, + "tip": "*Tip: U can also use the \"timeout\" discord feature with teh bot by **not** specifying any muted role in teh config!*" + }, "mute": { "already-mute": "Dis membr iz 'lready mute!", "already-unmute": "This mber iznt muted!", @@ -54,19 +67,9 @@ "list-title-0": "List ov mutd people in serv \"%{guild}\"", "list-title-1": "List ov a few (45) muted members of ur server \"%{guild}\"", "list-title-2": "Lits ov 60 muted members of dat guild \"%{guild}\"", - "mute-config-err": "Oops, cant do that. Plz check me perms on teh server 'nd try again.", - "mute-config-success": { - "other": "Teh role have been successffully created & configured! (%{count} channels couldnotbeconfigured, cuz I lack of perms)", - "zero": "Teh role have been successffully created & configured!" - }, - "mute-config-success2": { - "other": "The mouted role iz now correcty setup! (%{count} chats cannut be configured, cuz of a bruh lacking of perms)", - "zero": "The mouted role iz now correcty setup!" - }, "mute-high": "Ooops, 't sEEms dat `muted` rol iz tooo high 4 me to give it... Plz fiX dis problem by plac'ng my role higher than this nice `muted` role.", "no-mute": "Oooops, seemz dat teh nice `muted` role doznt exist :rofl: Creat'it nd assign perms yourself", - "no-mutes": "No member seems 2B banned from here", - "role-created": "Successsfully added da `muted` role! U can naw use `%{p}mute-config` cmd to ask me 2 config it meself", + "no-mutes": "No member seems 2B muted ._.", "staff-mute": "U cant prevent another cool staff member frm speek'ng ", "too-high": "Seemz that this membr iz tooooo high 4 me to time 'em out :ohno:" }, @@ -115,6 +118,7 @@ "one": "1 bad name edited .-.", "zero": "No bad name editd!" }, + "unhoist-too-many-members": "Uups, it's not that I'm lazy, u know... But oh wow, dat's a hell ov a big server u got here. Hell naw, i refuse to work dat much thanks.", "unmute-chat": "Teh mmber %{user} (%{userid}) canow speek 'gain", "warn": { "cant-bot": "Nope, cant warn anoder cool boat ^^", diff --git a/lang/permissions/en.json b/lang/permissions/en.json index d1e072ba..f3876af2 100644 --- a/lang/permissions/en.json +++ b/lang/permissions/en.json @@ -1,5 +1,9 @@ { - "general": "General permissions", + "channel": { + "channel": "In channel %{mention}", + "category": "In category %{name}", + "general": "General permissions" + }, "invalid_arg": "Invalid argument: %{arg}", "list": { "add_reactions": "Add reactions", @@ -45,6 +49,11 @@ "view_audit_log": "View audit logs", "view_guild_insights": "View guild insights" }, + "target": { + "member": "Member %{name}", + "role": "Role %{name}", + "value": "Value %{value}" + }, "title": "**\"%{name}\" permissions:**\n\n", "whatisthat": "What do they mean?" } \ No newline at end of file diff --git a/lang/permissions/fr.json b/lang/permissions/fr.json index 3da9211a..20224dcd 100644 --- a/lang/permissions/fr.json +++ b/lang/permissions/fr.json @@ -1,5 +1,9 @@ { - "general": "Permissions générales", + "channel": { + "channel": "Dans le salon %{mention}", + "category": "Dans la catégorie %{name}", + "general": "Permissions générales" + }, "invalid_arg": "Argument invalide : %{arg}", "list": { "add_reactions": "Ajouter des réactions", @@ -45,6 +49,11 @@ "view_audit_log": "Voir les logs du serveur", "view_guild_insights": "Voir les analyses de serveur" }, + "target": { + "member": "Membre %{name}", + "role": "Rôle %{name}", + "value": "Valeur %{value}" + }, "title": "**Permission de '%{name}' :**\n\n", "whatisthat": "De quoi s'agit-il ?" } \ No newline at end of file diff --git a/lang/permissions/lolcat.json b/lang/permissions/lolcat.json index 6284a821..7ef6f864 100644 --- a/lang/permissions/lolcat.json +++ b/lang/permissions/lolcat.json @@ -1,5 +1,9 @@ { - "general": "gLoBal pErmZ", + "channel": { + "channel": "4 channel %{mention}", + "category": "4 teh category %{name}", + "general": "Global perms or smth" + }, "invalid_arg": "Uh no, %{arg} iz no valid sir", "list": { "add_reactions": "Add reactions", @@ -45,6 +49,11 @@ "view_audit_log": "View audit logs", "view_guild_insights": "View guild insights" }, + "target": { + "member": "Human %{name}", + "role": "People %{name}", + "value": "Raw value %{value}" + }, "title": "**'%{name}' permissung:**\n\n", "whatisthat": "click HEEEERE if u wanna know" } \ No newline at end of file diff --git a/lang/roles_react/en.json b/lang/roles_react/en.json index 2e340c13..5c1a5aff 100644 --- a/lang/roles_react/en.json +++ b/lang/roles_react/en.json @@ -5,7 +5,7 @@ "embed-edited": "The embed and reactions have been successfully updated", "no-rr": "No role is related to this emoji", "not-zbot-embed": "This message does not contain any role-reaction embed", - "not-zbot-msg": "This message is not Zbot's", + "not-zbot-msg": "This message is not from me!", "reactions-edited": "The reactions have been correctly updated", "role-given": "The role %{r} has been given", "role-lost": "The role %{r} has been removed from your roles", diff --git a/lang/roles_react/fr.json b/lang/roles_react/fr.json index 2d1d72a2..a9c42e62 100644 --- a/lang/roles_react/fr.json +++ b/lang/roles_react/fr.json @@ -5,7 +5,7 @@ "embed-edited": "L'embed et les réactions ont bien été mises à jour", "no-rr": "Aucun rôle n'est lié à cet émoji", "not-zbot-embed": "Ce message ne contient pas d'embed de roles-reactions", - "not-zbot-msg": "Ce message n'est pas celui de Zbot", + "not-zbot-msg": "Ce message ne vient pas de moi !", "reactions-edited": "Les réactions ont bien été mises à jour", "role-given": "Le rôle %{r} a bien été donné", "role-lost": "Le rôle %{r} a bien été retiré", diff --git a/lang/roles_react/lolcat.json b/lang/roles_react/lolcat.json index 1f631215..edad8e52 100644 --- a/lang/roles_react/lolcat.json +++ b/lang/roles_react/lolcat.json @@ -5,7 +5,7 @@ "embed-edited": "Teh embed & reactions have bee succccessssffuly update", "no-rr": "NO role is related to this emoji", "not-zbot-embed": "This msg dont has any rolereact colorful box", - "not-zbot-msg": "NOOOOOOO tish msg is not Zbot's. bad dude.", + "not-zbot-msg": "Uh oh, dis msg is not from me 👀", "reactions-edited": "The react has been rightly updat :check:", "role-given": "The role %{r} has been given", "role-lost": "ThE rolE %{r} HAS BEeN reMoVEd fR0M yOUr ROLEs", diff --git a/lang/rss/de.json b/lang/rss/de.json index 9ee98ad0..0187e105 100644 --- a/lang/rss/de.json +++ b/lang/rss/de.json @@ -6,7 +6,6 @@ "deviant-default-flow": "{logo} | Neue Kreation von {author} : **{title}**\nVeröffentlicht am {date}\nLink: {link}\n{mentions}", "deviant-form-last": "{logo} | Hier ist die neuste Kreation von {author}:\n{title}\nVeröffentlicht am {date}\nLink: {url}", "embed-json-changed": "Der eingebettete Feed wurde verändert", - "fail-add": "Ein Fehler ist während dem antworten aufgetreten. Bitte versuche es später oder kontaktiere den Zbot Support (gebe den `about` Befehl für den Server Link ein)", "flow-limit": "Wegen auftretenden Fehlern, kannst du nicht mehr, als %{limit} rss Feeds pro Server haben.", "guild-complete": "%{count} rss Streams wurden in %{time} Sekunden neu geladen!", "guild-error": "Ein Fehler ist während dem Verfahren aufgetreten: `%{err}`\nWenn du denkst, dass es nicht dein Fehler war, gehe zu Zbot Support", diff --git a/lang/rss/en.json b/lang/rss/en.json index 195cf6dc..6dbd62d3 100644 --- a/lang/rss/en.json +++ b/lang/rss/en.json @@ -27,7 +27,6 @@ "deviant-default-flow": "{logo} | New creation by {author}: **{title}**\nPublished on {date}\nLink: {link}\n{mentions}", "deviant-form-last": "{logo} | Here is the new creation of {author}:\n{title}\nPublished on {date}\nLink: {url}", "embed-json-changed": "The embed of this feed has been modified", - "fail-add": "An error occurred while processing your response. Please try again later, or contact bot support (enter the command `about` for server link)", "flow-limit": "For performance reasons, you cannot track more than %{limit} rss feeds per server.", "guild-complete": "%{count} rss streams have been correctly reloaded, in %{time} seconds!", "guild-error": "An error occurred during the procedure: `%{err}`\nIf you think this error is not your own, you can report it to support", diff --git a/lang/rss/fi.json b/lang/rss/fi.json index 47c27260..51e99a3b 100644 --- a/lang/rss/fi.json +++ b/lang/rss/fi.json @@ -7,7 +7,6 @@ "deviant-default-flow": "{logo} | Uusi luomus tekijä: {author} : **{title}**\nJulkaistu {date}\nLinkki: {link}\n{mentions}", "deviant-form-last": "{logo} | Tässä uusi luomus tekijältä {author}:\n{title}\nJulkaistu {date}\nLinkki: {url}", "embed-json-changed": "Upotettu linkki tällä virtauksella on muokattu", - "fail-add": "Virhe tapahtui sinun vastauksen käsittelyssä. Yritä uudelleen myöhemmin, tai ota yhteyttä tukeen (kutsu linkin saat lähettämällä `about` komennon)", "flow-limit": "Suorituskykyjen syystä, et voi seurata enempää kuin %{limit} rss syötettä per palvelin.", "guild-complete": "%{count} rss suoratoistoa on päivitetty, %{time} sekunnissa!", "guild-error": "Virhe tapahtui menettelyssä: `%{err}`\nJos luulet että tämä virhe ei ole sinun, ota yhteyttä tukeen", diff --git a/lang/rss/fr.json b/lang/rss/fr.json index bea3eaae..60fa4eaa 100644 --- a/lang/rss/fr.json +++ b/lang/rss/fr.json @@ -27,7 +27,6 @@ "deviant-default-flow": "{logo} | Nouvelle création de {author} : **{title}**\nPubliée le {date}\nLien : {link}\n{mentions}", "deviant-form-last": "{logo} | Voici la dernière création de {author}:\n{title}\nPubliée le {date}\nLien : {url}", "embed-json-changed": "L'embed de ce flux a bien été modifié", - "fail-add": "Une erreur s'est produite lors du traitement de votre réponse. Merci de réessayer plus tard, ou de contacter le support du bot (entrez la commande `about` pour le lien du serveur)", "flow-limit": "Pour des raisons de performances, vous ne pouvez pas suivre plus de %{limit} flux rss par serveur.", "guild-complete": "%{count} flux rss ont correctement été rechargés, en %{time} secondes !", "guild-error": "Une erreur est survenue pendant la procédure : `%{err}`\nSi vous pensez que cette erreur ne vient pas de vous, vous pouvez en avertir le support", diff --git a/lang/rss/fr2.json b/lang/rss/fr2.json index 3d3ac26e..f6b4e3c6 100644 --- a/lang/rss/fr2.json +++ b/lang/rss/fr2.json @@ -7,7 +7,6 @@ "deviant-default-flow": "{logo} | Nouvelle création de {author} : **{title}**\nPubliée le {date}\nLien : {link}\n{mentions}", "deviant-form-last": "{logo} | Voici la dernière création de {author}:\n{title}\nPubliée le {date}\nLien : {url}", "embed-json-changed": "L'embed de ce flux a bien été modifié", - "fail-add": "Une erreur s'est produite lors du traitement de ta réponse :blobconfused:. Merci de réessayer plus tard, ou de contacter le support du bot (entrez la commande `about` pour le lien du serveur)", "flow-limit": "Pour des raisons de performances, tu peux pas suivre plus de %{limit} flux rss par serveur.", "guild-complete": "%{count} flux rss ont correctement été rechargés, en %{time} secondes !", "guild-error": "Une erreur est survenue pendant la procédure : `%{err}`\nSi tu penses que cette erreur ne vient pas de toi, avertis-en le support plz.", diff --git a/lang/rss/lolcat.json b/lang/rss/lolcat.json index c22b7371..d7514c1a 100644 --- a/lang/rss/lolcat.json +++ b/lang/rss/lolcat.json @@ -27,7 +27,6 @@ "deviant-default-flow": "{logo} | New draw by {author} : **{title}**\nDated on {date}\nURL: {link}\n{mentions}", "deviant-form-last": "{logo} | Here is da brand new thing of {author}:\n{title}\nDarwn on {date}\nZelda: {url}", "embed-json-changed": "The embed ov feed have been modifed", - "fail-add": "An fAtal erroR have occurred whale proczzing ur respond. Plz trye again laterz, r contakt boat suPPORt (entr teh comand `about` 4 srver link)", "flow-limit": "Fr pirformunce reesons, U can notz track mor than %{limit} rss feeds per srver.", "guild-complete": "%{count} rss streams haz correctly rechargd in %{time} seconds!", "guild-error": "A error occurrd durin teh load: `%{err}`\nIf you think dis err iz not your auwn, u can report it to support staff", diff --git a/lang/server/en.json b/lang/server/en.json index 4bc0e979..f644f8f4 100644 --- a/lang/server/en.json +++ b/lang/server/en.json @@ -1,5 +1,4 @@ { - "config-help": "This command is mainly used to configure your server. By doing `%{p}config see [option]` you will get an overview of the current configurations, and server administrators can enter `%{p}config change