From 0158dbab1d9576f5560a62dca1d707b647626881 Mon Sep 17 00:00:00 2001 From: Kowlin Date: Wed, 1 Dec 2021 21:35:30 +0100 Subject: [PATCH] Reorganize `[p]set` command group (#5432) * Reorganised Set command group * Moved custominfo * Tox styling * Make `set locale®ionalformat` groups work same as server subcommands * Use consistent method names for commands in `[p]set` group * Update command names in docstrings * Remove some weird rst formatting that bugs out my syntax highlighting * Add checks to some command groups * Update docs * oops * Minor fixes * Move `[p]set api` group and its subcommands to other command groups * Move `[p]set ownernotifications` group to other command groups * black reformat Co-authored-by: jack1142 <6032823+jack1142@users.noreply.github.com> --- docs/cog_guides/core.rst | 1015 ++++++++++-------- docs/getting_started.rst | 4 +- redbot/core/core_commands.py | 1909 ++++++++++++++++++---------------- 3 files changed, 1592 insertions(+), 1336 deletions(-) diff --git a/docs/cog_guides/core.rst b/docs/cog_guides/core.rst index 239a1ec25..a0edc6555 100644 --- a/docs/cog_guides/core.rst +++ b/docs/cog_guides/core.rst @@ -1565,7 +1565,7 @@ helpset resetformatter This resets Red's help formatter to the default formatter. **Example:** - - ``[p]helpset resetformatter```` + - ``[p]helpset resetformatter`` .. _core-command-helpset-resetsettings: @@ -1586,7 +1586,7 @@ This resets Red's help settings to their defaults. This may not have an impact when using custom formatters from 3rd party cogs **Example:** - - ``[p]helpset resetsettings```` + - ``[p]helpset resetsettings`` .. _core-command-helpset-showaliases: @@ -1660,7 +1660,7 @@ Show the current help settings. **Example:** - - ``[p]helpset showsettings```` + - ``[p]helpset showsettings`` .. _core-command-helpset-tagline: @@ -1919,8 +1919,6 @@ info Shows info about Red. -See ``[p]set custominfo`` to customize. - .. _core-command-invite: ^^^^^^ @@ -2742,71 +2740,6 @@ set Commands for changing Red's settings. -.. _core-command-set-addadminrole: - -"""""""""""""""" -set addadminrole -"""""""""""""""" - -.. note:: |guildowner-lock| - -**Syntax** - -.. code-block:: none - - [p]set addadminrole - -**Description** - -Adds an admin role for this guild. - -Admins have the same access as Mods, plus additional admin level commands like: - - ``[p]set serverprefix`` - - ``[p]addrole`` - - ``[p]ban`` - - ``[p]ignore guild`` - - And more. - - **Examples:** - - ``[p]set addadminrole @Admins`` - - ``[p]set addadminrole Super Admins`` - -**Arguments:** - - ```` - The role to add as an admin. - -.. _core-command-set-addmodrole: - -"""""""""""""" -set addmodrole -"""""""""""""" - -.. note:: |guildowner-lock| - -**Syntax** - -.. code-block:: none - - [p]set addmodrole - -**Description** - -Adds a moderator role for this guild. - -This grants access to moderator level commands like: - - ``[p]mute`` - - ``[p]cleanup`` - - ``[p]customcommand create`` - - And more. - - **Examples:** - - ``[p]set addmodrole @Mods`` - - ``[p]set addmodrole Loyal Helpers`` - -**Arguments:** - - ```` - The role to add as a moderator. - .. _core-command-set-api: """"""" @@ -2860,7 +2793,7 @@ Show all external API services along with their keys that have been set. Secrets are not shown. **Example:** - - ``[p]set api list```` + - ``[p]set api list`` .. _core-command-set-api-remove: @@ -2885,11 +2818,29 @@ Remove the given services with all their keys and tokens. **Arguments:** - ```` - The services to remove. -.. _core-command-set-avatar: +.. _core-command-set-bot: -"""""""""" -set avatar -"""""""""" +""""""" +set bot +""""""" + +.. note:: |admin-lock| + +**Syntax** + +.. code-block:: none + + [p]set bot + +**Description** + +Commands for changing Red's metadata. + +.. _core-command-set-bot-avatar: + +"""""""""""""" +set bot avatar +"""""""""""""" .. note:: |owner-lock| @@ -2897,7 +2848,7 @@ set avatar .. code-block:: none - [p]set avatar [url] + [p]set bot avatar [url] **Description** @@ -2906,18 +2857,18 @@ Sets Red's avatar Supports either an attachment or an image URL. **Examples:** - - ``[p]set avatar`` - With an image attachment, this will set the avatar. - - ``[p]set avatar`` - Without an attachment, this will show the command help. - - ``[p]set avatar https://links.flaree.xyz/k95`` - Sets the avatar to the provided url. + - ``[p]set bot avatar`` - With an image attachment, this will set the avatar. + - ``[p]set bot avatar`` - Without an attachment, this will show the command help. + - ``[p]set bot avatar https://links.flaree.xyz/k95`` - Sets the avatar to the provided url. **Arguments:** - ``[url]`` - An image url to be used as an avatar. Leave blank when uploading an attachment. -.. _core-command-set-avatar-remove: +.. _core-command-set-bot-avatar-remove: -""""""""""""""""" -set avatar remove -""""""""""""""""" +""""""""""""""""""""" +set bot avatar remove +""""""""""""""""""""" .. note:: |owner-lock| @@ -2925,16 +2876,137 @@ set avatar remove .. code-block:: none - [p]set avatar remove + [p]set bot avatar remove -.. tip:: Alias: ``set avatar clear`` +.. tip:: Alias: ``set bot avatar clear`` **Description** Removes Red's avatar. **Example:** - - ``[p]set avatar remove`` + - ``[p]set bot avatar remove`` + +.. _core-command-set-bot-custominfo: + +"""""""""""""""""" +set bot custominfo +"""""""""""""""""" + +.. note:: |owner-lock| + +**Syntax** + +.. code-block:: none + + [p]set bot custominfo [text] + +**Description** + +Customizes a section of ``[p]info``. + +The maximum amount of allowed characters is 1024. +Supports markdown, links and "mentions". + +Link example: ``[My link](https://example.com)`` + +**Examples:** + - ``[p]set bot custominfo >>> I can use **markdown** such as quotes, ||spoilers|| and multiple lines.`` + - ``[p]set bot custominfo Join my [support server](discord.gg/discord)!`` + - ``[p]set bot custominfo`` - Removes custom info text. + +**Arguments:** + - ``[text]`` - The custom info text. + +.. _core-command-set-bot-description: + +""""""""""""""""""" +set bot description +""""""""""""""""""" + +.. note:: |owner-lock| + +**Syntax** + +.. code-block:: none + + [p]set bot description [description] + +**Description** + +Sets the bot's description. + +Use without a description to reset. +This is shown in a few locations, including the help menu. + +The maximum description length is 250 characters to ensure it displays properly. + +The default is "Red V3". + +**Examples:** + - ``[p]set bot description`` - Resets the description to the default setting. + - ``[p]set bot description MyBot: A Red V3 Bot`` + +**Arguments:** + - ``[description]`` - The description to use for this bot. Leave blank to reset to the default. + +.. _core-command-set-bot-nickname: + +"""""""""""""""" +set bot nickname +"""""""""""""""" + +.. note:: |admin-lock| + +**Syntax** + +.. code-block:: none + + [p]set bot nickname [nickname] + +**Description** + +Sets Red's nickname for the current server. + +Maximum length for a nickname is 32 characters. + +**Example:** + - ``[p]set bot nickname 🎃 SpookyBot 🎃`` + +**Arguments:** + - ``[nickname]`` - The nickname to give the bot. Leave blank to clear the current nickname. + +.. _core-command-set-bot-username: + +"""""""""""""""" +set bot username +"""""""""""""""" + +.. note:: |owner-lock| + +**Syntax** + +.. code-block:: none + + [p]set bot username + +.. tip:: Alias: ``set bot name`` + +**Description** + +Sets Red's username. + +Maximum length for a username is 32 characters. + +.. Note:: The username of a verified bot cannot be manually changed. + + Please contact Discord support to change it. + +**Example:** + - ``[p]set bot username BaguetteBot`` + +**Arguments:** + - ```` - The username to give the bot. .. _core-command-set-colour: @@ -2970,66 +3042,6 @@ https://discordpy.readthedocs.io/en/stable/ext/commands/api.html#discord.ext.com **Arguments:** - ``[colour]`` - The colour to use for embeds. Leave blank to set to the default value (red). -.. _core-command-set-competing: - -""""""""""""" -set competing -""""""""""""" - -.. note:: |owner-lock| - -**Syntax** - -.. code-block:: none - - [p]set competing [competing] - -**Description** - -Sets Red's competing status. - -This will appear as ``Competing in ``. - -Maximum length for a competing status is 128 characters. - -**Examples:** - - ``[p]set competing`` - Clears the activity status. - - ``[p]set competing London 2012 Olympic Games`` - -**Arguments:** - - ``[competing]`` - The text to follow ``Competing in``. Leave blank to clear the current activity status. - -.. _core-command-set-custominfo: - -"""""""""""""" -set custominfo -"""""""""""""" - -.. note:: |owner-lock| - -**Syntax** - -.. code-block:: none - - [p]set custominfo [text] - -**Description** - -Customizes a section of ``[p]info``. - -The maximum amount of allowed characters is 1024. -Supports markdown, links and "mentions". - -Link example: ``[My link](https://example.com)`` - -**Examples:** - - ``[p]set custominfo >>> I can use **markdown** such as quotes, ||spoilers|| and multiple lines.`` - - ``[p]set custominfo Join my [support server](discord.gg/discord)!`` - - ``[p]set custominfo`` - Removes custom info text. - -**Arguments:** - - ``[text]`` - The custom info text. - .. _core-command-set-deletedelay: """"""""""""""" @@ -3062,38 +3074,6 @@ This is only applied to the current server and not globally. **Arguments:** - ``[time]`` - The seconds to wait before deleting the command message. Use -1 to disable. -.. _core-command-set-description: - -""""""""""""""" -set description -""""""""""""""" - -.. note:: |owner-lock| - -**Syntax** - -.. code-block:: none - - [p]set description [description] - -**Description** - -Sets the bot's description. - -Use without a description to reset. -This is shown in a few locations, including the help menu. - -The maximum description length is 250 characters to ensure it displays properly. - -The default is "Red V3". - -**Examples:** - - ``[p]set description`` - Resets the description to the default setting. - - ``[p]set description MyBot: A Red V3 Bot`` - -**Arguments:** - - ``[description]`` - The description to use for this bot. Leave blank to reset to the default. - .. _core-command-set-fuzzy: """"""""" @@ -3119,99 +3099,6 @@ Default is for fuzzy command search to be disabled. **Example:** - ``[p]set fuzzy`` -.. _core-command-set-globallocale: - -"""""""""""""""" -set globallocale -"""""""""""""""" - -.. note:: |owner-lock| - -**Syntax** - -.. code-block:: none - - [p]set globallocale - -**Description** - -Changes the bot's default locale. - -This will be used when a server has not set a locale, or in DMs. - -Go to `Red's Crowdin page `_ to see locales that are available with translations. - -To reset to English, use "en-US". - -**Examples:** - - ``[p]set locale en-US`` - - ``[p]set locale de-DE`` - - ``[p]set locale fr-FR`` - - ``[p]set locale pl-PL`` - -**Arguments:** - - ```` - The default locale to use for the bot. This can be any language code with country code included. - -.. _core-command-set-globalregionalformat: - -"""""""""""""""""""""""" -set globalregionalformat -"""""""""""""""""""""""" - -.. note:: |owner-lock| - -**Syntax** - -.. code-block:: none - - [p]set globalregionalformat [language_code] - -.. tip:: Alias: ``set globalregion`` - -**Description** - -Changes the bot's regional format. This is used for formatting date, time and numbers. - -``language_code`` can be any language code with country code included, e.g. ``en-US``, ``de-DE``, ``fr-FR``, ``pl-PL``, etc. -Leave ``language_code`` empty to base regional formatting on bot's locale. - -**Examples:** - - ``[p]set globalregionalformat en-US`` - - ``[p]set globalregion de-DE`` - - ``[p]set globalregionalformat`` - Resets to the locale. - -**Arguments:** - - ``[language_code]`` - The default region format to use for the bot. - -.. _core-command-set-listening: - -""""""""""""" -set listening -""""""""""""" - -.. note:: |owner-lock| - -**Syntax** - -.. code-block:: none - - [p]set listening [listening] - -**Description** - -Sets Red's listening status. - -This will appear as ``Listening to ``. - -Maximum length for a listening status is 128 characters. - -**Examples:** - - ``[p]set listening`` - Clears the activity status. - - ``[p]set listening jams`` - -**Arguments:** - - ``[listening]`` - The text to follow ``Listening to``. Leave blank to clear the current activity status. - .. _core-command-set-locale: """""""""" @@ -3233,7 +3120,8 @@ Changes the bot's locale in this server. Go to `Red's Crowdin page `_ to see locales that are available with translations. Use "default" to return to the bot's default set language. -To reset to English, use "en-US". + +If you want to change bot's global locale, see ``[p]set locale global`` command. **Examples:** - ``[p]set locale en-US`` @@ -3245,31 +3133,72 @@ To reset to English, use "en-US". **Arguments:** - ```` - The default locale to use for the bot. This can be any language code with country code included. -.. _core-command-set-nickname: +.. _core-command-set-locale-global: -"""""""""""" -set nickname -"""""""""""" +""""""""""""""""" +set locale global +""""""""""""""""" -.. note:: |admin-lock| +.. note:: |owner-lock| **Syntax** .. code-block:: none - [p]set nickname [nickname] + [p]set locale global **Description** -Sets Red's nickname for the current server. +Changes the bot's default locale. -Maximum length for a nickname is 32 characters. +This will be used when a server has not set a locale, or in DMs. -**Example:** - - ``[p]set nickname 🎃 SpookyBot 🎃`` +Go to `Red's Crowdin page `_ to see locales that are available with translations. + +To reset to English, use "en-US". + +**Examples:** + - ``[p]set locale global en-US`` + - ``[p]set locale global de-DE`` + - ``[p]set locale global fr-FR`` + - ``[p]set locale global pl-PL`` **Arguments:** - - ``[nickname]`` - The nickname to give the bot. Leave blank to clear the current nickname. + - ```` - The default locale to use for the bot. This can be any language code with country code included. + +.. _core-command-set-locale-server: + +""""""""""""""""" +set locale server +""""""""""""""""" + +.. note:: |guildowner-lock| + +**Syntax** + +.. code-block:: none + + [p]set locale server + +.. tip:: Aliases: ``set locale local``, ``set locale guild`` + +**Description** + +Changes the bot's locale in this server. + +Go to `Red's Crowdin page `_ to see locales that are available with translations. + +Use "default" to return to the bot's default set language. + +**Examples:** + - ``[p]set locale server en-US`` + - ``[p]set locale server de-DE`` + - ``[p]set locale server fr-FR`` + - ``[p]set locale server pl-PL`` + - ``[p]set locale server default`` - Resets to the global default locale. + +**Arguments:** + - ```` - The default locale to use for the bot. This can be any language code with country code included. .. _core-command-set-ownernotifications: @@ -3308,8 +3237,8 @@ set ownernotifications adddestination Adds a destination text channel to receive owner notifications. **Examples:** - - ``[p]ownernotifications adddestination #owner-notifications`` - - ``[p]ownernotifications adddestination 168091848718417920`` - Accepts channel IDs. + - ``[p]set ownernotifications adddestination #owner-notifications`` + - ``[p]set ownernotifications adddestination 168091848718417920`` - Accepts channel IDs. **Arguments:** - ```` - The channel to send owner notifications to. @@ -3331,7 +3260,7 @@ set ownernotifications listdestinations Lists the configured extra destinations for owner notifications. **Example:** - - ``[p]ownernotifications listdestinations`` + - ``[p]set ownernotifications listdestinations`` .. _core-command-set-ownernotifications-optin: @@ -3356,7 +3285,7 @@ This is the default state. Additional owners and destinations will not be affected. **Example:** - - ``[p]ownernotifications optin`` + - ``[p]set ownernotifications optin`` .. _core-command-set-ownernotifications-optout: @@ -3379,7 +3308,7 @@ Opt-out of receiving owner notifications. Additional owners and destinations will still receive notifications. **Example:** - - ``[p]ownernotifications optout`` + - ``[p]set ownernotifications optout`` .. _core-command-set-ownernotifications-removedestination: @@ -3400,43 +3329,12 @@ set ownernotifications removedestination Removes a destination text channel from receiving owner notifications. **Examples:** - - ``[p]ownernotifications removedestination #owner-notifications`` - - ``[p]ownernotifications deletedestination 168091848718417920`` - Accepts channel IDs. + - ``[p]set ownernotifications removedestination #owner-notifications`` + - ``[p]set ownernotifications deletedestination 168091848718417920`` - Accepts channel IDs. **Arguments:** - ```` - The channel to stop sending owner notifications to. -.. _core-command-set-playing: - -""""""""""" -set playing -""""""""""" - -.. note:: |owner-lock| - -**Syntax** - -.. code-block:: none - - [p]set playing [game] - -.. tip:: Alias: ``set game`` - -**Description** - -Sets Red's playing status. - -This will appear as ``Playing `` or ``PLAYING A GAME: `` depending on the context. - -Maximum length for a playing status is 128 characters. - -**Examples:** - - ``[p]set playing`` - Clears the activity status. - - ``[p]set playing the keyboard`` - -**Arguments:** - - ``[game]`` - The text to follow ``Playing``. Leave blank to clear the current activity status. - .. _core-command-set-prefix: """""""""" @@ -3483,7 +3381,7 @@ set regionalformat .. code-block:: none - [p]set regionalformat [language_code] + [p]set regionalformat .. tip:: Alias: ``set region`` @@ -3492,21 +3390,52 @@ set regionalformat Changes the bot's regional format in this server. This is used for formatting date, time and numbers. ``language_code`` can be any language code with country code included, e.g. ``en-US``, ``de-DE``, ``fr-FR``, ``pl-PL``, etc. -Leave ``language_code`` empty to base regional formatting on bot's locale in this server. +Pass "reset" to ``language_code`` to base regional formatting on bot's locale in this server. + +If you want to change bot's global regional format, see ``[p]set regionalformat global`` command. **Examples:** - ``[p]set regionalformat en-US`` - ``[p]set region de-DE`` - - ``[p]set regionalformat`` - Resets to the locale. + - ``[p]set regionalformat reset`` - Resets to the locale. **Arguments:** - ``[language_code]`` - The region format to use for the bot in this server. -.. _core-command-set-removeadminrole: +.. _core-command-set-regionalformat-global: -""""""""""""""""""" -set removeadminrole -""""""""""""""""""" +""""""""""""""""""""""""" +set regionalformat global +""""""""""""""""""""""""" + +.. note:: |owner-lock| + +**Syntax** + +.. code-block:: none + + [p]set regionalformat global + +**Description** + +Changes the bot's regional format. This is used for formatting date, time and numbers. + +``language_code`` can be any language code with country code included, e.g. ``en-US``, ``de-DE``, ``fr-FR``, ``pl-PL``, etc. +Pass "reset" to ``language_code`` to base regional formatting on bot's locale. + +**Examples:** + - ``[p]set regionalformat global en-US`` + - ``[p]set region global de-DE`` + - ``[p]set regionalformat global reset`` - Resets to the locale. + +**Arguments:** + - ``[language_code]`` - The default region format to use for the bot. + +.. _core-command-set-regionalformat-server: + +""""""""""""""""""""""""" +set regionalformat server +""""""""""""""""""""""""" .. note:: |guildowner-lock| @@ -3514,26 +3443,140 @@ set removeadminrole .. code-block:: none - [p]set removeadminrole + [p]set regionalformat server -.. tip:: Aliases: ``set remadmindrole``, ``set deladminrole``, ``set deleteadminrole`` +.. tip:: Aliases: ``set regionalformat local``, ``set regionalformat guild`` + +**Description** + +Changes the bot's regional format in this server. This is used for formatting date, time and numbers. + +``language_code`` can be any language code with country code included, e.g. ``en-US``, ``de-DE``, ``fr-FR``, ``pl-PL``, etc. +Pass "reset" to ``language_code`` to base regional formatting on bot's locale in this server. + +**Examples:** + - ``[p]set regionalformat server en-US`` + - ``[p]set region local de-DE`` + - ``[p]set regionalformat server reset`` - Resets to the locale. + +**Arguments:** + - ``[language_code]`` - The region format to use for the bot in this server. + +.. _core-command-set-roles: + +""""""""" +set roles +""""""""" + +.. note:: |guildowner-lock| + +**Syntax** + +.. code-block:: none + + [p]set roles + +**Description** + +Set server's admin and mod roles for Red. + +.. _core-command-set-roles-addadminrole: + +"""""""""""""""""""""" +set roles addadminrole +"""""""""""""""""""""" + +.. note:: |guildowner-lock| + +**Syntax** + +.. code-block:: none + + [p]set roles addadminrole + +**Description** + +Adds an admin role for this guild. + +Admins have the same access as Mods, plus additional admin level commands like: + - ``[p]set serverprefix`` + - ``[p]addrole`` + - ``[p]ban`` + - ``[p]ignore guild`` + + And more. + + **Examples:** + - ``[p]set roles addadminrole @Admins`` + - ``[p]set roles addadminrole Super Admins`` + +**Arguments:** + - ```` - The role to add as an admin. + +.. _core-command-set-roles-addmodrole: + +"""""""""""""""""""" +set roles addmodrole +"""""""""""""""""""" + +.. note:: |guildowner-lock| + +**Syntax** + +.. code-block:: none + + [p]set roles addmodrole + +**Description** + +Adds a moderator role for this guild. + +This grants access to moderator level commands like: + - ``[p]mute`` + - ``[p]cleanup`` + - ``[p]customcommand create`` + + And more. + + **Examples:** + - ``[p]set roles addmodrole @Mods`` + - ``[p]set roles addmodrole Loyal Helpers`` + +**Arguments:** + - ```` - The role to add as a moderator. + +.. _core-command-set-roles-removeadminrole: + +""""""""""""""""""""""""" +set roles removeadminrole +""""""""""""""""""""""""" + +.. note:: |guildowner-lock| + +**Syntax** + +.. code-block:: none + + [p]set roles removeadminrole + +.. tip:: Aliases: ``set roles remadmindrole``, ``set roles deladminrole``, ``set roles deleteadminrole`` **Description** Removes an admin role for this guild. **Examples:** - - ``[p]set removeadminrole @Admins`` - - ``[p]set removeadminrole Super Admins`` + - ``[p]set roles removeadminrole @Admins`` + - ``[p]set roles removeadminrole Super Admins`` **Arguments:** - ```` - The role to remove from being an admin. -.. _core-command-set-removemodrole: +.. _core-command-set-roles-removemodrole: -""""""""""""""""" -set removemodrole -""""""""""""""""" +""""""""""""""""""""""" +set roles removemodrole +""""""""""""""""""""""" .. note:: |guildowner-lock| @@ -3541,17 +3584,17 @@ set removemodrole .. code-block:: none - [p]set removemodrole + [p]set roles removemodrole -.. tip:: Aliases: ``set remmodrole``, ``set delmodrole``, ``set deletemodrole`` +.. tip:: Aliases: ``set roles remmodrole``, ``set roles delmodrole``, ``set roles deletemodrole`` **Description** Removes a mod role for this guild. **Examples:** - - ``[p]set removemodrole @Mods`` - - ``[p]set removemodrole Loyal Helpers`` + - ``[p]set roles removemodrole @Mods`` + - ``[p]set roles removemodrole Loyal Helpers`` **Arguments:** - ```` - The role to remove from being a moderator. @@ -3647,30 +3690,17 @@ set status .. code-block:: none - [p]set status + [p]set status **Description** -Sets Red's status. +Commands for setting Red's status. -Available statuses: - - ``online`` - - ``idle`` - - ``dnd`` - - ``invisible`` +.. _core-command-set-status-competing: -**Examples:** - - ``[p]set status online`` - Clears the status. - - ``[p]set status invisible`` - -**Arguments:** - - ```` - One of the available statuses. - -.. _core-command-set-streaming: - -""""""""""""" -set streaming -""""""""""""" +"""""""""""""""""""" +set status competing +"""""""""""""""""""" .. note:: |owner-lock| @@ -3678,9 +3708,176 @@ set streaming .. code-block:: none - [p]set streaming [( )] + [p]set status competing [competing] -.. tip:: Aliases: ``set stream``, ``set twitch`` +**Description** + +Sets Red's competing status. + +This will appear as ``Competing in ``. + +Maximum length for a competing status is 128 characters. + +**Examples:** + - ``[p]set status competing`` - Clears the activity status. + - ``[p]set status competing London 2012 Olympic Games`` + +**Arguments:** + - ``[competing]`` - The text to follow ``Competing in``. Leave blank to clear the current activity status. + +.. _core-command-set-status-dnd: + +"""""""""""""" +set status dnd +"""""""""""""" + +.. note:: |owner-lock| + +**Syntax** + +.. code-block:: none + + [p]set status dnd + +.. tip:: Aliases: ``set status donotdisturb``, ``set status busy`` + +**Description** + +Sets Red's status to do not disturb. + +.. _core-command-set-status-idle: + +""""""""""""""" +set status idle +""""""""""""""" + +.. note:: |owner-lock| + +**Syntax** + +.. code-block:: none + + [p]set status idle + +.. tip:: Aliases: ``set status away``, ``set status afk`` + +**Description** + +Sets Red's status to idle. + +.. _core-command-set-status-invisible: + +"""""""""""""""""""" +set status invisible +"""""""""""""""""""" + +.. note:: |owner-lock| + +**Syntax** + +.. code-block:: none + + [p]set status invisible + +.. tip:: Alias: ``set status offline`` + +**Description** + +Sets Red's status to invisible. + +.. _core-command-set-status-listening: + +"""""""""""""""""""" +set status listening +"""""""""""""""""""" + +.. note:: |owner-lock| + +**Syntax** + +.. code-block:: none + + [p]set status listening [listening] + +**Description** + +Sets Red's listening status. + +This will appear as ``Listening to ``. + +Maximum length for a listening status is 128 characters. + +**Examples:** + - ``[p]set status listening`` - Clears the activity status. + - ``[p]set status listening jams`` + +**Arguments:** + - ``[listening]`` - The text to follow ``Listening to``. Leave blank to clear the current activity status. + +.. _core-command-set-status-online: + +""""""""""""""""" +set status online +""""""""""""""""" + +.. note:: |owner-lock| + +**Syntax** + +.. code-block:: none + + [p]set status online + +**Description** + +Sets Red's status to online. + +.. _core-command-set-status-playing: + +"""""""""""""""""" +set status playing +"""""""""""""""""" + +.. note:: |owner-lock| + +**Syntax** + +.. code-block:: none + + [p]set status playing [game] + +.. tip:: Alias: ``set status game`` + +**Description** + +Sets Red's playing status. + +This will appear as ``Playing `` or ``PLAYING A GAME: `` depending on the context. + +Maximum length for a playing status is 128 characters. + +**Examples:** + - ``[p]set status playing`` - Clears the activity status. + - ``[p]set status playing the keyboard`` + +**Arguments:** + - ``[game]`` - The text to follow ``Playing``. Leave blank to clear the current activity status. + +.. _core-command-set-status-streaming: + +"""""""""""""""""""" +set status streaming +"""""""""""""""""""" + +.. note:: |owner-lock| + +**Syntax** + +.. code-block:: none + + [p]set status streaming [( )] + +.. tip:: Aliases: ``set status stream``, ``set status twitch`` **Description** @@ -3694,14 +3891,43 @@ Maximum length for a stream title is 128 characters. Leaving both streamer and stream_title empty will clear it. **Examples:** - - ``[p]set stream`` - Clears the activity status. - - ``[p]set stream 26 Twentysix is streaming`` - Sets the stream to ``https://www.twitch.tv/26``. - - ``[p]set stream https://twitch.tv/26 Twentysix is streaming`` - Sets the URL manually. + - ``[p]set status stream`` - Clears the activity status. + - ``[p]set status stream 26 Twentysix is streaming`` - Sets the stream to ``https://www.twitch.tv/26``. + - ``[p]set status stream https://twitch.tv/26 Twentysix is streaming`` - Sets the URL manually. **Arguments:** - ```` - The twitch streamer to provide a link to. This can be their twitch name or the entire URL. - ```` - The text to follow ``Streaming`` in the status. +.. _core-command-set-status-watching: + +""""""""""""""""""" +set status watching +""""""""""""""""""" + +.. note:: |owner-lock| + +**Syntax** + +.. code-block:: none + + [p]set status watching [watching] + +**Description** + +Sets Red's watching status. + +This will appear as ``Watching ``. + +Maximum length for a watching status is 128 characters. + +**Examples:** + - ``[p]set status watching`` - Clears the activity status. + - ``[p]set status watching [p]help`` + +**Arguments:** + - ``[watching]`` - The text to follow ``Watching``. Leave blank to clear the current activity status. + .. _core-command-set-usebotcolour: """""""""""""""" @@ -3728,67 +3954,6 @@ Otherwise, the colour used will be the colour of the bot's top role. **Example:** - ``[p]set usebotcolour`` -.. _core-command-set-username: - -"""""""""""" -set username -"""""""""""" - -.. note:: |owner-lock| - -**Syntax** - -.. code-block:: none - - [p]set username - -.. tip:: Alias: ``set name`` - -**Description** - -Sets Red's username. - -Maximum length for a username is 32 characters. - -.. Note:: The username of a verified bot cannot be manually changed. - - Please contact Discord support to change it. - -**Example:** - - ``[p]set username BaguetteBot`` - -**Arguments:** - - ```` - The username to give the bot. - -.. _core-command-set-watching: - -"""""""""""" -set watching -"""""""""""" - -.. note:: |owner-lock| - -**Syntax** - -.. code-block:: none - - [p]set watching [watching] - -**Description** - -Sets Red's watching status. - -This will appear as ``Watching ``. - -Maximum length for a watching status is 128 characters. - -**Examples:** - - ``[p]set watching`` - Clears the activity status. - - ``[p]set watching [p]help`` - -**Arguments:** - - ``[watching]`` - The text to follow ``Watching``. Leave blank to clear the current activity status. - .. _core-command-shutdown: ^^^^^^^^ diff --git a/docs/getting_started.rst b/docs/getting_started.rst index bfad407fe..f65a53643 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -225,7 +225,7 @@ Administrator ~~~~~~~~~~~~~ The administrator is defined by its roles. You can set multiple admin roles -with the ``[p]set addadminrole`` and ``[p]set removeadminrole`` commands. +with the ``[p]set roles addadminrole`` and ``[p]set roles removeadminrole`` commands. For example, in the mod cog, an admin can use the ``[p]modset`` command which defines the cog settings. @@ -235,7 +235,7 @@ Moderator ~~~~~~~~~ A moderator is a step above the average users. You can set multiple moderator -roles with the ``[p]set addmodrole`` and ``[p]set removemodrole`` commands. +roles with the ``[p]set roles addmodrole`` and ``[p]set roles removemodrole`` commands. For example, in the filter cog, a mod will be able to use the various commands under ``[p]filter`` (such as adding and removing filtered words), but they will diff --git a/redbot/core/core_commands.py b/redbot/core/core_commands.py index f71186f5c..1e3c5fa00 100644 --- a/redbot/core/core_commands.py +++ b/redbot/core/core_commands.py @@ -417,10 +417,7 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic): @commands.command() async def info(self, ctx: commands.Context): - """Shows info about [botname]. - - See `[p]set custominfo` to customize. - """ + """Shows info about [botname].""" embed_links = await ctx.embed_requested() author_repo = "https://github.com/Twentysix26" org_repo = "https://github.com/Cog-Creators" @@ -2349,8 +2346,986 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic): async def _set(self, ctx: commands.Context): """Commands for changing [botname]'s settings.""" - @_set.command("showsettings") - async def set_showsettings(self, ctx: commands.Context): + # -- Bot Metadata Commands -- ### + + @_set.group(name="bot", aliases=["metadata"]) + @checks.admin_or_permissions(manage_nicknames=True) + async def _set_bot(self, ctx: commands.Context): + """Commands for changing [botname]'s metadata.""" + + @checks.is_owner() + @_set_bot.command(name="description") + async def _set_bot_description(self, ctx: commands.Context, *, description: str = ""): + """ + Sets the bot's description. + + Use without a description to reset. + This is shown in a few locations, including the help menu. + + The maximum description length is 250 characters to ensure it displays properly. + + The default is "Red V3". + + **Examples:** + - `[p]set bot description` - Resets the description to the default setting. + - `[p]set bot description MyBot: A Red V3 Bot` + + **Arguments:** + - `[description]` - The description to use for this bot. Leave blank to reset to the default. + """ + if not description: + await ctx.bot._config.description.clear() + ctx.bot.description = "Red V3" + await ctx.send(_("Description reset.")) + elif len(description) > 250: # While the limit is 256, we bold it adding characters. + await ctx.send( + _( + "This description is too long to properly display. " + "Please try again with below 250 characters." + ) + ) + else: + await ctx.bot._config.description.set(description) + ctx.bot.description = description + await ctx.tick() + + @_set_bot.group(name="avatar", invoke_without_command=True) + @checks.is_owner() + async def _set_bot_avatar(self, ctx: commands.Context, url: str = None): + """Sets [botname]'s avatar + + Supports either an attachment or an image URL. + + **Examples:** + - `[p]set bot avatar` - With an image attachment, this will set the avatar. + - `[p]set bot avatar` - Without an attachment, this will show the command help. + - `[p]set bot avatar https://links.flaree.xyz/k95` - Sets the avatar to the provided url. + + **Arguments:** + - `[url]` - An image url to be used as an avatar. Leave blank when uploading an attachment. + """ + if len(ctx.message.attachments) > 0: # Attachments take priority + data = await ctx.message.attachments[0].read() + elif url is not None: + if url.startswith("<") and url.endswith(">"): + url = url[1:-1] + + async with aiohttp.ClientSession() as session: + try: + async with session.get(url) as r: + data = await r.read() + except aiohttp.InvalidURL: + return await ctx.send(_("That URL is invalid.")) + except aiohttp.ClientError: + return await ctx.send(_("Something went wrong while trying to get the image.")) + else: + await ctx.send_help() + return + + try: + async with ctx.typing(): + await ctx.bot.user.edit(avatar=data) + except discord.HTTPException: + await ctx.send( + _( + "Failed. Remember that you can edit my avatar " + "up to two times a hour. The URL or attachment " + "must be a valid image in either JPG or PNG format." + ) + ) + except discord.InvalidArgument: + await ctx.send(_("JPG / PNG format only.")) + else: + await ctx.send(_("Done.")) + + @_set_bot_avatar.command(name="remove", aliases=["clear"]) + @checks.is_owner() + async def _set_bot_avatar_remove(self, ctx: commands.Context): + """ + Removes [botname]'s avatar. + + **Example:** + - `[p]set bot avatar remove` + """ + async with ctx.typing(): + await ctx.bot.user.edit(avatar=None) + await ctx.send(_("Avatar removed.")) + + @_set_bot.command(name="username", aliases=["name"]) + @checks.is_owner() + async def _set_bot_username(self, ctx: commands.Context, *, username: str): + """Sets [botname]'s username. + + Maximum length for a username is 32 characters. + + Note: The username of a verified bot cannot be manually changed. + Please contact Discord support to change it. + + **Example:** + - `[p]set bot username BaguetteBot` + + **Arguments:** + - `` - The username to give the bot. + """ + try: + if self.bot.user.public_flags.verified_bot: + await ctx.send( + _( + "The username of a verified bot cannot be manually changed." + " Please contact Discord support to change it." + ) + ) + return + if len(username) > 32: + await ctx.send(_("Failed to change name. Must be 32 characters or fewer.")) + return + async with ctx.typing(): + await asyncio.wait_for(self._name(name=username), timeout=30) + except asyncio.TimeoutError: + await ctx.send( + _( + "Changing the username timed out. " + "Remember that you can only do it up to 2 times an hour." + " Use nicknames if you need frequent changes: {command}" + ).format(command=inline(f"{ctx.clean_prefix}set bot nickname")) + ) + except discord.HTTPException as e: + if e.code == 50035: + error_string = e.text.split("\n")[1] # Remove the "Invalid Form body" + await ctx.send( + _( + "Failed to change the username. " + "Discord returned the following error:\n" + "{error_message}" + ).format(error_message=inline(error_string)) + ) + else: + log.error( + "Unexpected error occurred when trying to change the username.", exc_info=e + ) + await ctx.send(_("Unexpected error occurred when trying to change the username.")) + else: + await ctx.send(_("Done.")) + + @_set_bot.command(name="nickname") + @checks.admin_or_permissions(manage_nicknames=True) + @commands.guild_only() + async def _set_bot_nickname(self, ctx: commands.Context, *, nickname: str = None): + """Sets [botname]'s nickname for the current server. + + Maximum length for a nickname is 32 characters. + + **Example:** + - `[p]set bot nickname 🎃 SpookyBot 🎃` + + **Arguments:** + - `[nickname]` - The nickname to give the bot. Leave blank to clear the current nickname. + """ + try: + if nickname and len(nickname) > 32: + await ctx.send(_("Failed to change nickname. Must be 32 characters or fewer.")) + return + await ctx.guild.me.edit(nick=nickname) + except discord.Forbidden: + await ctx.send(_("I lack the permissions to change my own nickname.")) + else: + await ctx.send(_("Done.")) + + @_set_bot.command(name="custominfo") + @checks.is_owner() + async def _set_bot_custominfo(self, ctx: commands.Context, *, text: str = None): + """Customizes a section of `[p]info`. + + The maximum amount of allowed characters is 1024. + Supports markdown, links and "mentions". + + Link example: `[My link](https://example.com)` + + **Examples:** + - `[p]set bot custominfo >>> I can use **markdown** such as quotes, ||spoilers|| and multiple lines.` + - `[p]set bot custominfo Join my [support server](discord.gg/discord)!` + - `[p]set bot custominfo` - Removes custom info text. + + **Arguments:** + - `[text]` - The custom info text. + """ + if not text: + await ctx.bot._config.custom_info.clear() + await ctx.send(_("The custom text has been cleared.")) + return + if len(text) <= 1024: + await ctx.bot._config.custom_info.set(text) + await ctx.send(_("The custom text has been set.")) + await ctx.invoke(self.info) + else: + await ctx.send(_("Text must be fewer than 1024 characters long.")) + + # -- End Bot Metadata Commands -- ### + # -- Bot Status Commands -- ### + + @_set.group(name="status") + @checks.bot_in_a_guild() + @checks.is_owner() + async def _set_status(self, ctx: commands.Context): + """Commands for setting [botname]'s status.""" + + @_set_status.command( + name="streaming", aliases=["stream", "twitch"], usage="[( )]" + ) + @checks.bot_in_a_guild() + @checks.is_owner() + async def _set_status_stream(self, ctx: commands.Context, streamer=None, *, stream_title=None): + """Sets [botname]'s streaming status to a twitch stream. + + This will appear as `Streaming ` or `LIVE ON TWITCH` depending on the context. + It will also include a `Watch` button with a twitch.tv url for the provided streamer. + + Maximum length for a stream title is 128 characters. + + Leaving both streamer and stream_title empty will clear it. + + **Examples:** + - `[p]set status stream` - Clears the activity status. + - `[p]set status stream 26 Twentysix is streaming` - Sets the stream to `https://www.twitch.tv/26`. + - `[p]set status stream https://twitch.tv/26 Twentysix is streaming` - Sets the URL manually. + + **Arguments:** + - `` - The twitch streamer to provide a link to. This can be their twitch name or the entire URL. + - `` - The text to follow `Streaming` in the status.""" + status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else None + + if stream_title: + stream_title = stream_title.strip() + if "twitch.tv/" not in streamer: + streamer = "https://www.twitch.tv/" + streamer + if len(streamer) > 511: + await ctx.send(_("The maximum length of the streamer url is 511 characters.")) + return + if len(stream_title) > 128: + await ctx.send(_("The maximum length of the stream title is 128 characters.")) + return + activity = discord.Streaming(url=streamer, name=stream_title) + await ctx.bot.change_presence(status=status, activity=activity) + elif streamer is not None: + await ctx.send_help() + return + else: + await ctx.bot.change_presence(activity=None, status=status) + await ctx.send(_("Done.")) + + @_set_status.command(name="playing", aliases=["game"]) + @checks.bot_in_a_guild() + @checks.is_owner() + async def _set_status_game(self, ctx: commands.Context, *, game: str = None): + """Sets [botname]'s playing status. + + This will appear as `Playing ` or `PLAYING A GAME: ` depending on the context. + + Maximum length for a playing status is 128 characters. + + **Examples:** + - `[p]set status playing` - Clears the activity status. + - `[p]set status playing the keyboard` + + **Arguments:** + - `[game]` - The text to follow `Playing`. Leave blank to clear the current activity status. + """ + + if game: + if len(game) > 128: + await ctx.send(_("The maximum length of game descriptions is 128 characters.")) + return + game = discord.Game(name=game) + else: + game = None + status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online + await ctx.bot.change_presence(status=status, activity=game) + if game: + await ctx.send(_("Status set to ``Playing {game.name}``.").format(game=game)) + else: + await ctx.send(_("Game cleared.")) + + @_set_status.command(name="listening") + @checks.bot_in_a_guild() + @checks.is_owner() + async def _set_status_listening(self, ctx: commands.Context, *, listening: str = None): + """Sets [botname]'s listening status. + + This will appear as `Listening to `. + + Maximum length for a listening status is 128 characters. + + **Examples:** + - `[p]set status listening` - Clears the activity status. + - `[p]set status listening jams` + + **Arguments:** + - `[listening]` - The text to follow `Listening to`. Leave blank to clear the current activity status.""" + + status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online + if listening: + if len(listening) > 128: + await ctx.send( + _("The maximum length of listening descriptions is 128 characters.") + ) + return + activity = discord.Activity(name=listening, type=discord.ActivityType.listening) + else: + activity = None + await ctx.bot.change_presence(status=status, activity=activity) + if activity: + await ctx.send( + _("Status set to ``Listening to {listening}``.").format(listening=listening) + ) + else: + await ctx.send(_("Listening cleared.")) + + @_set_status.command(name="watching") + @checks.bot_in_a_guild() + @checks.is_owner() + async def _set_status_watching(self, ctx: commands.Context, *, watching: str = None): + """Sets [botname]'s watching status. + + This will appear as `Watching `. + + Maximum length for a watching status is 128 characters. + + **Examples:** + - `[p]set status watching` - Clears the activity status. + - `[p]set status watching [p]help` + + **Arguments:** + - `[watching]` - The text to follow `Watching`. Leave blank to clear the current activity status.""" + + status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online + if watching: + if len(watching) > 128: + await ctx.send(_("The maximum length of watching descriptions is 128 characters.")) + return + activity = discord.Activity(name=watching, type=discord.ActivityType.watching) + else: + activity = None + await ctx.bot.change_presence(status=status, activity=activity) + if activity: + await ctx.send(_("Status set to ``Watching {watching}``.").format(watching=watching)) + else: + await ctx.send(_("Watching cleared.")) + + @_set_status.command(name="competing") + @checks.bot_in_a_guild() + @checks.is_owner() + async def _set_status_competing(self, ctx: commands.Context, *, competing: str = None): + """Sets [botname]'s competing status. + + This will appear as `Competing in `. + + Maximum length for a competing status is 128 characters. + + **Examples:** + - `[p]set status competing` - Clears the activity status. + - `[p]set status competing London 2012 Olympic Games` + + **Arguments:** + - `[competing]` - The text to follow `Competing in`. Leave blank to clear the current activity status.""" + + status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online + if competing: + if len(competing) > 128: + await ctx.send( + _("The maximum length of competing descriptions is 128 characters.") + ) + return + activity = discord.Activity(name=competing, type=discord.ActivityType.competing) + else: + activity = None + await ctx.bot.change_presence(status=status, activity=activity) + if activity: + await ctx.send( + _("Status set to ``Competing in {competing}``.").format(competing=competing) + ) + else: + await ctx.send(_("Competing cleared.")) + + async def _set_my_status(self, ctx: commands.Context, status: discord.Status): + game = ctx.bot.guilds[0].me.activity if len(ctx.bot.guilds) > 0 else None + await ctx.bot.change_presence(status=status, activity=game) + return await ctx.send(_("Status changed to {}.").format(status)) + + @_set_status.command(name="online") + @checks.bot_in_a_guild() + @checks.is_owner() + async def _set_status_online(self, ctx: commands.Context): + """Set [botname]'s status to online.""" + await self._set_my_status(ctx, discord.Status.online) + + @_set_status.command(name="dnd", aliases=["donotdisturb", "busy"]) + @checks.bot_in_a_guild() + @checks.is_owner() + async def _set_status_dnd(self, ctx: commands.Context): + """Set [botname]'s status to do not disturb.""" + await self._set_my_status(ctx, discord.Status.do_not_disturb) + + @_set_status.command(name="idle", aliases=["away", "afk"]) + @checks.bot_in_a_guild() + @checks.is_owner() + async def _set_status_idle(self, ctx: commands.Context): + """Set [botname]'s status to idle.""" + await self._set_my_status(ctx, discord.Status.idle) + + @_set_status.command(name="invisible", aliases=["offline"]) + @checks.bot_in_a_guild() + @checks.is_owner() + async def _set_status_invisible(self, ctx: commands.Context): + """Set [botname]'s status to invisible.""" + await self._set_my_status(ctx, discord.Status.invisible) + + # -- End Bot Status Commands -- ### + # -- Bot Roles Commands -- ### + + @_set.group(name="roles") + @checks.guildowner() + @commands.guild_only() + async def _set_roles(self, ctx: commands.Context): + """Set server's admin and mod roles for [botname].""" + + @_set_roles.command(name="addadminrole") + @checks.guildowner() + @commands.guild_only() + async def _set_roles_addadminrole(self, ctx: commands.Context, *, role: discord.Role): + """ + Adds an admin role for this server. + + Admins have the same access as Mods, plus additional admin level commands like: + - `[p]set serverprefix` + - `[p]addrole` + - `[p]ban` + - `[p]ignore guild` + + And more. + + **Examples:** + - `[p]set roles addadminrole @Admins` + - `[p]set roles addadminrole Super Admins` + + **Arguments:** + - `` - The role to add as an admin. + """ + async with ctx.bot._config.guild(ctx.guild).admin_role() as roles: + if role.id in roles: + return await ctx.send(_("This role is already an admin role.")) + roles.append(role.id) + await ctx.send(_("That role is now considered an admin role.")) + + @_set_roles.command(name="addmodrole") + @checks.guildowner() + @commands.guild_only() + async def _set_roles_addmodrole(self, ctx: commands.Context, *, role: discord.Role): + """ + Adds a moderator role for this server. + + This grants access to moderator level commands like: + - `[p]mute` + - `[p]cleanup` + - `[p]customcommand create` + + And more. + + **Examples:** + - `[p]set roles addmodrole @Mods` + - `[p]set roles addmodrole Loyal Helpers` + + **Arguments:** + - `` - The role to add as a moderator. + """ + async with ctx.bot._config.guild(ctx.guild).mod_role() as roles: + if role.id in roles: + return await ctx.send(_("This role is already a mod role.")) + roles.append(role.id) + await ctx.send(_("That role is now considered a mod role.")) + + @_set_roles.command( + name="removeadminrole", aliases=["remadmindrole", "deladminrole", "deleteadminrole"] + ) + @checks.guildowner() + @commands.guild_only() + async def _set_roles_removeadminrole(self, ctx: commands.Context, *, role: discord.Role): + """ + Removes an admin role for this server. + + **Examples:** + - `[p]set roles removeadminrole @Admins` + - `[p]set roles removeadminrole Super Admins` + + **Arguments:** + - `` - The role to remove from being an admin. + """ + async with ctx.bot._config.guild(ctx.guild).admin_role() as roles: + if role.id not in roles: + return await ctx.send(_("That role was not an admin role to begin with.")) + roles.remove(role.id) + await ctx.send(_("That role is no longer considered an admin role.")) + + @_set_roles.command( + name="removemodrole", aliases=["remmodrole", "delmodrole", "deletemodrole"] + ) + @checks.guildowner() + @commands.guild_only() + async def _set_roles_removemodrole(self, ctx: commands.Context, *, role: discord.Role): + """ + Removes a mod role for this server. + + **Examples:** + - `[p]set roles removemodrole @Mods` + - `[p]set roles removemodrole Loyal Helpers` + + **Arguments:** + - `` - The role to remove from being a moderator. + """ + async with ctx.bot._config.guild(ctx.guild).mod_role() as roles: + if role.id not in roles: + return await ctx.send(_("That role was not a mod role to begin with.")) + roles.remove(role.id) + await ctx.send(_("That role is no longer considered a mod role.")) + + # -- End Set Roles Commands -- ### + # -- Set Locale Commands -- ### + + @_set.group(name="locale", invoke_without_command=True) + @checks.guildowner_or_permissions(manage_guild=True) + async def _set_locale(self, ctx: commands.Context, language_code: str): + """ + Changes [botname]'s locale in this server. + + Go to [Red's Crowdin page](https://translate.discord.red) to see locales that are available with translations. + + Use "default" to return to the bot's default set language. + + If you want to change bot's global locale, see `[p]set locale global` command. + + **Examples:** + - `[p]set locale en-US` + - `[p]set locale de-DE` + - `[p]set locale fr-FR` + - `[p]set locale pl-PL` + - `[p]set locale default` - Resets to the global default locale. + + **Arguments:** + - `` - The default locale to use for the bot. This can be any language code with country code included. + """ + if ctx.guild is None: + await ctx.send_help() + return + await ctx.invoke(self._set_locale_local, language_code) + + @_set_locale.command(name="global") + @checks.is_owner() + async def _set_locale_global(self, ctx: commands.Context, language_code: str): + """ + Changes [botname]'s default locale. + + This will be used when a server has not set a locale, or in DMs. + + Go to [Red's Crowdin page](https://translate.discord.red) to see locales that are available with translations. + + To reset to English, use "en-US". + + **Examples:** + - `[p]set locale global en-US` + - `[p]set locale global de-DE` + - `[p]set locale global fr-FR` + - `[p]set locale global pl-PL` + + **Arguments:** + - `` - The default locale to use for the bot. This can be any language code with country code included. + """ + try: + locale = BabelLocale.parse(language_code, sep="-") + except (ValueError, UnknownLocaleError): + await ctx.send(_("Invalid language code. Use format: `en-US`")) + return + if locale.territory is None: + await ctx.send( + _("Invalid format - language code has to include country code, e.g. `en-US`") + ) + return + standardized_locale_name = f"{locale.language}-{locale.territory}" + i18n.set_locale(standardized_locale_name) + await self.bot._i18n_cache.set_locale(None, standardized_locale_name) + await i18n.set_contextual_locales_from_guild(self.bot, ctx.guild) + await ctx.send(_("Global locale has been set.")) + + @_set_locale.command(name="server", aliases=["local", "guild"]) + @commands.guild_only() + @checks.guildowner_or_permissions(manage_guild=True) + async def _set_locale_local(self, ctx: commands.Context, language_code: str): + """ + Changes [botname]'s locale in this server. + + Go to [Red's Crowdin page](https://translate.discord.red) to see locales that are available with translations. + + Use "default" to return to the bot's default set language. + + **Examples:** + - `[p]set locale server en-US` + - `[p]set locale server de-DE` + - `[p]set locale server fr-FR` + - `[p]set locale server pl-PL` + - `[p]set locale server default` - Resets to the global default locale. + + **Arguments:** + - `` - The default locale to use for the bot. This can be any language code with country code included. + """ + if language_code.lower() == "default": + global_locale = await self.bot._config.locale() + i18n.set_contextual_locale(global_locale) + await self.bot._i18n_cache.set_locale(ctx.guild, None) + await ctx.send(_("Locale has been set to the default.")) + return + try: + locale = BabelLocale.parse(language_code, sep="-") + except (ValueError, UnknownLocaleError): + await ctx.send(_("Invalid language code. Use format: `en-US`")) + return + if locale.territory is None: + await ctx.send( + _("Invalid format - language code has to include country code, e.g. `en-US`") + ) + return + standardized_locale_name = f"{locale.language}-{locale.territory}" + i18n.set_contextual_locale(standardized_locale_name) + await self.bot._i18n_cache.set_locale(ctx.guild, standardized_locale_name) + await ctx.send(_("Locale has been set.")) + + @_set.group(name="regionalformat", aliases=["region"], invoke_without_command=True) + @checks.guildowner_or_permissions(manage_guild=True) + async def _set_regional_format(self, ctx: commands.Context, language_code: str): + """ + Changes the bot's regional format in this server. This is used for formatting date, time and numbers. + + `language_code` can be any language code with country code included, e.g. `en-US`, `de-DE`, `fr-FR`, `pl-PL`, etc. + Pass "reset" to `language_code` to base regional formatting on bot's locale in this server. + + If you want to change bot's global regional format, see `[p]set regionalformat global` command. + + **Examples:** + - `[p]set regionalformat en-US` + - `[p]set region de-DE` + - `[p]set regionalformat reset` - Resets to the locale. + + **Arguments:** + - `[language_code]` - The region format to use for the bot in this server. + """ + if ctx.guild is None: + await ctx.send_help() + return + await ctx.invoke(self._set_regional_format_local, language_code) + + @_set_regional_format.command(name="global") + @checks.is_owner() + async def _set_regional_format_global(self, ctx: commands.Context, language_code: str): + """ + Changes the bot's regional format. This is used for formatting date, time and numbers. + + `language_code` can be any language code with country code included, e.g. `en-US`, `de-DE`, `fr-FR`, `pl-PL`, etc. + Pass "reset" to `language_code` to base regional formatting on bot's locale. + + **Examples:** + - `[p]set regionalformat global en-US` + - `[p]set region global de-DE` + - `[p]set regionalformat global reset` - Resets to the locale. + + **Arguments:** + - `[language_code]` - The default region format to use for the bot. + """ + if language_code.lower() == "reset": + i18n.set_regional_format(None) + await self.bot._i18n_cache.set_regional_format(None, None) + await ctx.send(_("Global regional formatting will now be based on bot's locale.")) + return + + try: + locale = BabelLocale.parse(language_code, sep="-") + except (ValueError, UnknownLocaleError): + await ctx.send(_("Invalid language code. Use format: `en-US`")) + return + if locale.territory is None: + await ctx.send( + _("Invalid format - language code has to include country code, e.g. `en-US`") + ) + return + standardized_locale_name = f"{locale.language}-{locale.territory}" + i18n.set_regional_format(standardized_locale_name) + await self.bot._i18n_cache.set_regional_format(None, standardized_locale_name) + await ctx.send( + _("Global regional formatting will now be based on `{language_code}` locale.").format( + language_code=standardized_locale_name + ) + ) + + @_set_regional_format.command(name="server", aliases=["local", "guild"]) + @commands.guild_only() + @checks.guildowner_or_permissions(manage_guild=True) + async def _set_regional_format_local(self, ctx: commands.Context, language_code: str): + """ + Changes the bot's regional format in this server. This is used for formatting date, time and numbers. + + `language_code` can be any language code with country code included, e.g. `en-US`, `de-DE`, `fr-FR`, `pl-PL`, etc. + Pass "reset" to `language_code` to base regional formatting on bot's locale in this server. + + **Examples:** + - `[p]set regionalformat server en-US` + - `[p]set region local de-DE` + - `[p]set regionalformat server reset` - Resets to the locale. + + **Arguments:** + - `[language_code]` - The region format to use for the bot in this server. + """ + if language_code.lower() == "reset": + i18n.set_contextual_regional_format(None) + await self.bot._i18n_cache.set_regional_format(ctx.guild, None) + await ctx.send( + _("Regional formatting will now be based on bot's locale in this server.") + ) + return + + try: + locale = BabelLocale.parse(language_code, sep="-") + except (ValueError, UnknownLocaleError): + await ctx.send(_("Invalid language code. Use format: `en-US`")) + return + if locale.territory is None: + await ctx.send( + _("Invalid format - language code has to include country code, e.g. `en-US`") + ) + return + standardized_locale_name = f"{locale.language}-{locale.territory}" + i18n.set_contextual_regional_format(standardized_locale_name) + await self.bot._i18n_cache.set_regional_format(ctx.guild, standardized_locale_name) + await ctx.send( + _("Regional formatting will now be based on `{language_code}` locale.").format( + language_code=standardized_locale_name + ) + ) + + # -- End Set Locale Commands -- ### + # -- Set Api Commands -- ### + + @_set.group(name="api", invoke_without_command=True) + @checks.is_owner() + async def _set_api(self, ctx: commands.Context, service: str, *, tokens: TokenConverter): + """ + Commands to set, list or remove various external API tokens. + + This setting will be asked for by some 3rd party cogs and some core cogs. + + To add the keys provide the service name and the tokens as a comma separated + list of key,values as described by the cog requesting this command. + + Note: API tokens are sensitive, so this command should only be used in a private channel or in DM with the bot. + + **Examples:** + - `[p]set api Spotify redirect_uri localhost` + - `[p]set api github client_id,whoops client_secret,whoops` + + **Arguments:** + - `` - The service you're adding tokens to. + - `` - Pairs of token keys and values. The key and value should be separated by one of ` `, `,`, or `;`. + """ + if ctx.channel.permissions_for(ctx.me).manage_messages: + await ctx.message.delete() + await ctx.bot.set_shared_api_tokens(service, **tokens) + await ctx.send(_("`{service}` API tokens have been set.").format(service=service)) + + @_set_api.command(name="list") + async def _set_api_list(self, ctx: commands.Context): + """ + Show all external API services along with their keys that have been set. + + Secrets are not shown. + + **Example:** + - `[p]set api list`` + """ + + services: dict = await ctx.bot.get_shared_api_tokens() + if not services: + await ctx.send(_("No API services have been set yet.")) + return + + sorted_services = sorted(services.keys(), key=str.lower) + + joined = _("Set API services:\n") if len(services) > 1 else _("Set API service:\n") + for service_name in sorted_services: + joined += "+ {}\n".format(service_name) + for key_name in services[service_name].keys(): + joined += " - {}\n".format(key_name) + for page in pagify(joined, ["\n"], shorten_by=16): + await ctx.send(box(page.lstrip(" "), lang="diff")) + + @_set_api.command(name="remove", require_var_positional=True) + async def _set_api_remove(self, ctx: commands.Context, *services: str): + """ + Remove the given services with all their keys and tokens. + + **Examples:** + - `[p]set api remove Spotify` + - `[p]set api remove github audiodb` + + **Arguments:** + - `` - The services to remove.""" + bot_services = (await ctx.bot.get_shared_api_tokens()).keys() + services = [s for s in services if s in bot_services] + + if services: + await self.bot.remove_shared_api_services(*services) + if len(services) > 1: + msg = _("Services deleted successfully:\n{services_list}").format( + services_list=humanize_list(services) + ) + else: + msg = _("Service deleted successfully: {service_name}").format( + service_name=services[0] + ) + await ctx.send(msg) + else: + await ctx.send(_("None of the services you provided had any keys set.")) + + # -- End Set Api Commands -- ### + # -- Set Ownernotifications Commands -- ### + + @checks.is_owner() + @_set.group(name="ownernotifications") + async def _set_ownernotifications(self, ctx: commands.Context): + """ + Commands for configuring owner notifications. + + Owner notifications include usage of `[p]contact` and available Red updates. + """ + pass + + @_set_ownernotifications.command(name="optin") + async def _set_ownernotifications_optin(self, ctx: commands.Context): + """ + Opt-in on receiving owner notifications. + + This is the default state. + + Note: This will only resume sending owner notifications to your DMs. + Additional owners and destinations will not be affected. + + **Example:** + - `[p]set ownernotifications optin` + """ + async with ctx.bot._config.owner_opt_out_list() as opt_outs: + if ctx.author.id in opt_outs: + opt_outs.remove(ctx.author.id) + + await ctx.tick() + + @_set_ownernotifications.command(name="optout") + async def _set_ownernotifications_optout(self, ctx: commands.Context): + """ + Opt-out of receiving owner notifications. + + Note: This will only stop sending owner notifications to your DMs. + Additional owners and destinations will still receive notifications. + + **Example:** + - `[p]set ownernotifications optout` + """ + async with ctx.bot._config.owner_opt_out_list() as opt_outs: + if ctx.author.id not in opt_outs: + opt_outs.append(ctx.author.id) + + await ctx.tick() + + @_set_ownernotifications.command(name="adddestination") + async def _set_ownernotifications_adddestination( + self, ctx: commands.Context, *, channel: Union[discord.TextChannel, int] + ): + """ + Adds a destination text channel to receive owner notifications. + + **Examples:** + - `[p]set ownernotifications adddestination #owner-notifications` + - `[p]set ownernotifications adddestination 168091848718417920` - Accepts channel IDs. + + **Arguments:** + - `` - The channel to send owner notifications to. + """ + + try: + channel_id = channel.id + except AttributeError: + channel_id = channel + + async with ctx.bot._config.extra_owner_destinations() as extras: + if channel_id not in extras: + extras.append(channel_id) + + await ctx.tick() + + @_set_ownernotifications.command( + name="removedestination", aliases=["remdestination", "deletedestination", "deldestination"] + ) + async def _set_ownernotifications_removedestination( + self, ctx: commands.Context, *, channel: Union[discord.TextChannel, int] + ): + """ + Removes a destination text channel from receiving owner notifications. + + **Examples:** + - `[p]set ownernotifications removedestination #owner-notifications` + - `[p]set ownernotifications deletedestination 168091848718417920` - Accepts channel IDs. + + **Arguments:** + - `` - The channel to stop sending owner notifications to. + """ + + try: + channel_id = channel.id + except AttributeError: + channel_id = channel + + async with ctx.bot._config.extra_owner_destinations() as extras: + if channel_id in extras: + extras.remove(channel_id) + + await ctx.tick() + + @_set_ownernotifications.command(name="listdestinations") + async def _set_ownernotifications_listdestinations(self, ctx: commands.Context): + """ + Lists the configured extra destinations for owner notifications. + + **Example:** + - `[p]set ownernotifications listdestinations` + """ + + channel_ids = await ctx.bot._config.extra_owner_destinations() + + if not channel_ids: + await ctx.send(_("There are no extra channels being sent to.")) + return + + data = [] + + for channel_id in channel_ids: + channel = ctx.bot.get_channel(channel_id) + if channel: + # This includes the channel name in case the user can't see the channel. + data.append(f"{channel.mention} {channel} ({channel.id})") + else: + data.append(_("Unknown channel with id: {id}").format(id=channel_id)) + + output = "\n".join(data) + for page in pagify(output): + await ctx.send(page) + + # -- End Set Ownernotifications Commands -- ### + + @_set.command(name="showsettings") + async def _set_showsettings(self, ctx: commands.Context): """ Show the current settings for [botname]. """ @@ -2359,10 +3334,12 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic): guild = ctx.guild admin_role_ids = guild_data["admin_role"] admin_role_names = [r.name for r in guild.roles if r.id in admin_role_ids] - admin_roles_str = humanize_list(admin_role_names) if admin_role_names else "Not Set." + admin_roles_str = ( + humanize_list(admin_role_names) if admin_role_names else _("Not Set.") + ) mod_role_ids = guild_data["mod_role"] mod_role_names = [r.name for r in guild.roles if r.id in mod_role_ids] - mod_roles_str = humanize_list(mod_role_names) if mod_role_names else "Not Set." + mod_roles_str = humanize_list(mod_role_names) if mod_role_names else _("Not Set.") guild_locale = await i18n.get_locale_from_guild(self.bot, ctx.guild) guild_regional_format = ( @@ -2411,7 +3388,7 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic): @checks.guildowner_or_permissions(administrator=True) @_set.command(name="deletedelay") @commands.guild_only() - async def deletedelay(self, ctx: commands.Context, time: int = None): + async def _set_deletedelay(self, ctx: commands.Context, time: int = None): """Set the delay until the bot removes the command message. Must be between -1 and 60. @@ -2449,141 +3426,10 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic): else: await ctx.send(_("I will not delete command messages.")) - @checks.is_owner() - @_set.command(name="description") - async def setdescription(self, ctx: commands.Context, *, description: str = ""): - """ - Sets the bot's description. - - Use without a description to reset. - This is shown in a few locations, including the help menu. - - The maximum description length is 250 characters to ensure it displays properly. - - The default is "Red V3". - - **Examples:** - - `[p]set description` - Resets the description to the default setting. - - `[p]set description MyBot: A Red V3 Bot` - - **Arguments:** - - `[description]` - The description to use for this bot. Leave blank to reset to the default. - """ - if not description: - await ctx.bot._config.description.clear() - ctx.bot.description = "Red V3" - await ctx.send(_("Description reset.")) - elif len(description) > 250: # While the limit is 256, we bold it adding characters. - await ctx.send( - _( - "This description is too long to properly display. " - "Please try again with below 250 characters." - ) - ) - else: - await ctx.bot._config.description.set(description) - ctx.bot.description = description - await ctx.tick() - - @_set.command() + @_set.command(name="usebotcolour", aliases=["usebotcolor"]) @checks.guildowner() @commands.guild_only() - async def addadminrole(self, ctx: commands.Context, *, role: discord.Role): - """ - Adds an admin role for this guild. - - Admins have the same access as Mods, plus additional admin level commands like: - - `[p]set serverprefix` - - `[p]addrole` - - `[p]ban` - - `[p]ignore guild` - - And more. - - **Examples:** - - `[p]set addadminrole @Admins` - - `[p]set addadminrole Super Admins` - - **Arguments:** - - `` - The role to add as an admin. - """ - async with ctx.bot._config.guild(ctx.guild).admin_role() as roles: - if role.id in roles: - return await ctx.send(_("This role is already an admin role.")) - roles.append(role.id) - await ctx.send(_("That role is now considered an admin role.")) - - @_set.command() - @checks.guildowner() - @commands.guild_only() - async def addmodrole(self, ctx: commands.Context, *, role: discord.Role): - """ - Adds a moderator role for this guild. - - This grants access to moderator level commands like: - - `[p]mute` - - `[p]cleanup` - - `[p]customcommand create` - - And more. - - **Examples:** - - `[p]set addmodrole @Mods` - - `[p]set addmodrole Loyal Helpers` - - **Arguments:** - - `` - The role to add as a moderator. - """ - async with ctx.bot._config.guild(ctx.guild).mod_role() as roles: - if role.id in roles: - return await ctx.send(_("This role is already a mod role.")) - roles.append(role.id) - await ctx.send(_("That role is now considered a mod role.")) - - @_set.command(aliases=["remadmindrole", "deladminrole", "deleteadminrole"]) - @checks.guildowner() - @commands.guild_only() - async def removeadminrole(self, ctx: commands.Context, *, role: discord.Role): - """ - Removes an admin role for this guild. - - **Examples:** - - `[p]set removeadminrole @Admins` - - `[p]set removeadminrole Super Admins` - - **Arguments:** - - `` - The role to remove from being an admin. - """ - async with ctx.bot._config.guild(ctx.guild).admin_role() as roles: - if role.id not in roles: - return await ctx.send(_("That role was not an admin role to begin with.")) - roles.remove(role.id) - await ctx.send(_("That role is no longer considered an admin role.")) - - @_set.command(aliases=["remmodrole", "delmodrole", "deletemodrole"]) - @checks.guildowner() - @commands.guild_only() - async def removemodrole(self, ctx: commands.Context, *, role: discord.Role): - """ - Removes a mod role for this guild. - - **Examples:** - - `[p]set removemodrole @Mods` - - `[p]set removemodrole Loyal Helpers` - - **Arguments:** - - `` - The role to remove from being a moderator. - """ - async with ctx.bot._config.guild(ctx.guild).mod_role() as roles: - if role.id not in roles: - return await ctx.send(_("That role was not a mod role to begin with.")) - roles.remove(role.id) - await ctx.send(_("That role is no longer considered a mod role.")) - - @_set.command(aliases=["usebotcolor"]) - @checks.guildowner() - @commands.guild_only() - async def usebotcolour(self, ctx: commands.Context): + async def _set_usebotcolour(self, ctx: commands.Context): """ Toggle whether to use the bot owner-configured colour for embeds. @@ -2601,10 +3447,10 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic): ) ) - @_set.command() + @_set.command(name="serverfuzzy") @checks.guildowner() @commands.guild_only() - async def serverfuzzy(self, ctx: commands.Context): + async def _set_serverfuzzy(self, ctx: commands.Context): """ Toggle whether to enable fuzzy command search for the server. @@ -2625,9 +3471,9 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic): ) ) - @_set.command() + @_set.command(name="fuzzy") @checks.is_owner() - async def fuzzy(self, ctx: commands.Context): + async def _set_fuzzy(self, ctx: commands.Context): """ Toggle whether to enable fuzzy command search in DMs. @@ -2646,9 +3492,9 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic): ) ) - @_set.command(aliases=["color"]) + @_set.command(name="colour", aliases=["color"]) @checks.is_owner() - async def colour(self, ctx: commands.Context, *, colour: discord.Colour = None): + async def _set_colour(self, ctx: commands.Context, *, colour: discord.Colour = None): """ Sets a default colour to be used for the bot's embeds. @@ -2674,365 +3520,13 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic): await ctx.bot._config.color.set(colour.value) await ctx.send(_("The color has been set.")) - @_set.group(invoke_without_command=True) - @checks.is_owner() - async def avatar(self, ctx: commands.Context, url: str = None): - """Sets [botname]'s avatar - - Supports either an attachment or an image URL. - - **Examples:** - - `[p]set avatar` - With an image attachment, this will set the avatar. - - `[p]set avatar` - Without an attachment, this will show the command help. - - `[p]set avatar https://links.flaree.xyz/k95` - Sets the avatar to the provided url. - - **Arguments:** - - `[url]` - An image url to be used as an avatar. Leave blank when uploading an attachment. - """ - if len(ctx.message.attachments) > 0: # Attachments take priority - data = await ctx.message.attachments[0].read() - elif url is not None: - if url.startswith("<") and url.endswith(">"): - url = url[1:-1] - - async with aiohttp.ClientSession() as session: - try: - async with session.get(url) as r: - data = await r.read() - except aiohttp.InvalidURL: - return await ctx.send(_("That URL is invalid.")) - except aiohttp.ClientError: - return await ctx.send(_("Something went wrong while trying to get the image.")) - else: - await ctx.send_help() - return - - try: - async with ctx.typing(): - await ctx.bot.user.edit(avatar=data) - except discord.HTTPException: - await ctx.send( - _( - "Failed. Remember that you can edit my avatar " - "up to two times a hour. The URL or attachment " - "must be a valid image in either JPG or PNG format." - ) - ) - except discord.InvalidArgument: - await ctx.send(_("JPG / PNG format only.")) - else: - await ctx.send(_("Done.")) - - @avatar.command(name="remove", aliases=["clear"]) - @checks.is_owner() - async def avatar_remove(self, ctx: commands.Context): - """ - Removes [botname]'s avatar. - - **Example:** - - `[p]set avatar remove` - """ - async with ctx.typing(): - await ctx.bot.user.edit(avatar=None) - await ctx.send(_("Avatar removed.")) - - @_set.command(name="playing", aliases=["game"]) - @checks.bot_in_a_guild() - @checks.is_owner() - async def _game(self, ctx: commands.Context, *, game: str = None): - """Sets [botname]'s playing status. - - This will appear as `Playing ` or `PLAYING A GAME: ` depending on the context. - - Maximum length for a playing status is 128 characters. - - **Examples:** - - `[p]set playing` - Clears the activity status. - - `[p]set playing the keyboard` - - **Arguments:** - - `[game]` - The text to follow `Playing`. Leave blank to clear the current activity status. - """ - - if game: - if len(game) > 128: - await ctx.send(_("The maximum length of game descriptions is 128 characters.")) - return - game = discord.Game(name=game) - else: - game = None - status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online - await ctx.bot.change_presence(status=status, activity=game) - if game: - await ctx.send(_("Status set to ``Playing {game.name}``.").format(game=game)) - else: - await ctx.send(_("Game cleared.")) - - @_set.command(name="listening") - @checks.bot_in_a_guild() - @checks.is_owner() - async def _listening(self, ctx: commands.Context, *, listening: str = None): - """Sets [botname]'s listening status. - - This will appear as `Listening to `. - - Maximum length for a listening status is 128 characters. - - **Examples:** - - `[p]set listening` - Clears the activity status. - - `[p]set listening jams` - - **Arguments:** - - `[listening]` - The text to follow `Listening to`. Leave blank to clear the current activity status.""" - - status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online - if listening: - if len(listening) > 128: - await ctx.send( - _("The maximum length of listening descriptions is 128 characters.") - ) - return - activity = discord.Activity(name=listening, type=discord.ActivityType.listening) - else: - activity = None - await ctx.bot.change_presence(status=status, activity=activity) - if activity: - await ctx.send( - _("Status set to ``Listening to {listening}``.").format(listening=listening) - ) - else: - await ctx.send(_("Listening cleared.")) - - @_set.command(name="watching") - @checks.bot_in_a_guild() - @checks.is_owner() - async def _watching(self, ctx: commands.Context, *, watching: str = None): - """Sets [botname]'s watching status. - - This will appear as `Watching `. - - Maximum length for a watching status is 128 characters. - - **Examples:** - - `[p]set watching` - Clears the activity status. - - `[p]set watching [p]help` - - **Arguments:** - - `[watching]` - The text to follow `Watching`. Leave blank to clear the current activity status.""" - - status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online - if watching: - if len(watching) > 128: - await ctx.send(_("The maximum length of watching descriptions is 128 characters.")) - return - activity = discord.Activity(name=watching, type=discord.ActivityType.watching) - else: - activity = None - await ctx.bot.change_presence(status=status, activity=activity) - if activity: - await ctx.send(_("Status set to ``Watching {watching}``.").format(watching=watching)) - else: - await ctx.send(_("Watching cleared.")) - - @_set.command(name="competing") - @checks.bot_in_a_guild() - @checks.is_owner() - async def _competing(self, ctx: commands.Context, *, competing: str = None): - """Sets [botname]'s competing status. - - This will appear as `Competing in `. - - Maximum length for a competing status is 128 characters. - - **Examples:** - - `[p]set competing` - Clears the activity status. - - `[p]set competing London 2012 Olympic Games` - - **Arguments:** - - `[competing]` - The text to follow `Competing in`. Leave blank to clear the current activity status.""" - - status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online - if competing: - if len(competing) > 128: - await ctx.send( - _("The maximum length of competing descriptions is 128 characters.") - ) - return - activity = discord.Activity(name=competing, type=discord.ActivityType.competing) - else: - activity = None - await ctx.bot.change_presence(status=status, activity=activity) - if activity: - await ctx.send( - _("Status set to ``Competing in {competing}``.").format(competing=competing) - ) - else: - await ctx.send(_("Competing cleared.")) - - @_set.command() - @checks.bot_in_a_guild() - @checks.is_owner() - async def status(self, ctx: commands.Context, *, status: str): - """Sets [botname]'s status. - - Available statuses: - - `online` - - `idle` - - `dnd` - - `invisible` - - **Examples:** - - `[p]set status online` - Clears the status. - - `[p]set status invisible` - - **Arguments:** - - `` - One of the available statuses. - """ - - statuses = { - "online": discord.Status.online, - "idle": discord.Status.idle, - "dnd": discord.Status.dnd, - "invisible": discord.Status.invisible, - } - - game = ctx.bot.guilds[0].me.activity if len(ctx.bot.guilds) > 0 else None - try: - status = statuses[status.lower()] - except KeyError: - await ctx.send_help() - else: - await ctx.bot.change_presence(status=status, activity=game) - await ctx.send(_("Status changed to {}.").format(status)) - @_set.command( - name="streaming", aliases=["stream", "twitch"], usage="[( )]" + name="prefix", + aliases=["prefixes", "globalprefix", "globalprefixes"], + require_var_positional=True, ) - @checks.bot_in_a_guild() @checks.is_owner() - async def stream(self, ctx: commands.Context, streamer=None, *, stream_title=None): - """Sets [botname]'s streaming status to a twitch stream. - - This will appear as `Streaming ` or `LIVE ON TWITCH` depending on the context. - It will also include a `Watch` button with a twitch.tv url for the provided streamer. - - Maximum length for a stream title is 128 characters. - - Leaving both streamer and stream_title empty will clear it. - - **Examples:** - - `[p]set stream` - Clears the activity status. - - `[p]set stream 26 Twentysix is streaming` - Sets the stream to `https://www.twitch.tv/26`. - - `[p]set stream https://twitch.tv/26 Twentysix is streaming` - Sets the URL manually. - - **Arguments:** - - `` - The twitch streamer to provide a link to. This can be their twitch name or the entire URL. - - `` - The text to follow `Streaming` in the status.""" - - status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else None - - if stream_title: - stream_title = stream_title.strip() - if "twitch.tv/" not in streamer: - streamer = "https://www.twitch.tv/" + streamer - if len(streamer) > 511: - await ctx.send(_("The maximum length of the streamer url is 511 characters.")) - return - if len(stream_title) > 128: - await ctx.send(_("The maximum length of the stream title is 128 characters.")) - return - activity = discord.Streaming(url=streamer, name=stream_title) - await ctx.bot.change_presence(status=status, activity=activity) - elif streamer is not None: - await ctx.send_help() - return - else: - await ctx.bot.change_presence(activity=None, status=status) - await ctx.send(_("Done.")) - - @_set.command(name="username", aliases=["name"]) - @checks.is_owner() - async def _username(self, ctx: commands.Context, *, username: str): - """Sets [botname]'s username. - - Maximum length for a username is 32 characters. - - Note: The username of a verified bot cannot be manually changed. - Please contact Discord support to change it. - - **Example:** - - `[p]set username BaguetteBot` - - **Arguments:** - - `` - The username to give the bot. - """ - try: - if self.bot.user.public_flags.verified_bot: - await ctx.send( - _( - "The username of a verified bot cannot be manually changed." - " Please contact Discord support to change it." - ) - ) - return - if len(username) > 32: - await ctx.send(_("Failed to change name. Must be 32 characters or fewer.")) - return - async with ctx.typing(): - await asyncio.wait_for(self._name(name=username), timeout=30) - except asyncio.TimeoutError: - await ctx.send( - _( - "Changing the username timed out. " - "Remember that you can only do it up to 2 times an hour." - " Use nicknames if you need frequent changes: {command}" - ).format(command=inline(f"{ctx.clean_prefix}set nickname")) - ) - except discord.HTTPException as e: - if e.code == 50035: - error_string = e.text.split("\n")[1] # Remove the "Invalid Form body" - await ctx.send( - _( - "Failed to change the username. " - "Discord returned the following error:\n" - "{error_message}" - ).format(error_message=inline(error_string)) - ) - else: - log.error( - "Unexpected error occurred when trying to change the username.", exc_info=e - ) - await ctx.send(_("Unexpected error occurred when trying to change the username.")) - else: - await ctx.send(_("Done.")) - - @_set.command(name="nickname") - @checks.admin_or_permissions(manage_nicknames=True) - @commands.guild_only() - async def _nickname(self, ctx: commands.Context, *, nickname: str = None): - """Sets [botname]'s nickname for the current server. - - Maximum length for a nickname is 32 characters. - - **Example:** - - `[p]set nickname 🎃 SpookyBot 🎃` - - **Arguments:** - - `[nickname]` - The nickname to give the bot. Leave blank to clear the current nickname. - """ - try: - if nickname and len(nickname) > 32: - await ctx.send(_("Failed to change nickname. Must be 32 characters or fewer.")) - return - await ctx.guild.me.edit(nick=nickname) - except discord.Forbidden: - await ctx.send(_("I lack the permissions to change my own nickname.")) - else: - await ctx.send(_("Done.")) - - @_set.command(aliases=["prefixes"], require_var_positional=True) - @checks.is_owner() - async def prefix(self, ctx: commands.Context, *prefixes: str): + async def _set_prefix(self, ctx: commands.Context, *prefixes: str): """Sets [botname]'s global prefix(es). Warning: This is not additive. It will replace all current prefixes. @@ -3072,10 +3566,10 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic): else: await ctx.send(_("Prefixes set.")) - @_set.command(aliases=["serverprefixes"]) + @_set.command(name="serverprefix", aliases=["serverprefixes"]) @checks.admin_or_permissions(manage_guild=True) @commands.guild_only() - async def serverprefix(self, ctx: commands.Context, *prefixes: str): + async def _set_serverprefix(self, ctx: commands.Context, *prefixes: str): """ Sets [botname]'s server prefix(es). @@ -3106,282 +3600,6 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic): else: await ctx.send(_("Server prefixes set.")) - @_set.command() - @checks.is_owner() - async def globallocale(self, ctx: commands.Context, language_code: str): - """ - Changes the bot's default locale. - - This will be used when a server has not set a locale, or in DMs. - - Go to [Red's Crowdin page](https://translate.discord.red) to see locales that are available with translations. - - To reset to English, use "en-US". - - **Examples:** - - `[p]set locale en-US` - - `[p]set locale de-DE` - - `[p]set locale fr-FR` - - `[p]set locale pl-PL` - - **Arguments:** - - `` - The default locale to use for the bot. This can be any language code with country code included. - """ - try: - locale = BabelLocale.parse(language_code, sep="-") - except (ValueError, UnknownLocaleError): - await ctx.send(_("Invalid language code. Use format: `en-US`")) - return - if locale.territory is None: - await ctx.send( - _("Invalid format - language code has to include country code, e.g. `en-US`") - ) - return - standardized_locale_name = f"{locale.language}-{locale.territory}" - i18n.set_locale(standardized_locale_name) - await self.bot._i18n_cache.set_locale(None, standardized_locale_name) - await i18n.set_contextual_locales_from_guild(self.bot, ctx.guild) - await ctx.send(_("Global locale has been set.")) - - @_set.command() - @commands.guild_only() - @checks.guildowner_or_permissions(manage_guild=True) - async def locale(self, ctx: commands.Context, language_code: str): - """ - Changes the bot's locale in this server. - - Go to [Red's Crowdin page](https://translate.discord.red) to see locales that are available with translations. - - Use "default" to return to the bot's default set language. - To reset to English, use "en-US". - - **Examples:** - - `[p]set locale en-US` - - `[p]set locale de-DE` - - `[p]set locale fr-FR` - - `[p]set locale pl-PL` - - `[p]set locale default` - Resets to the global default locale. - - **Arguments:** - - `` - The default locale to use for the bot. This can be any language code with country code included. - """ - if language_code.lower() == "default": - global_locale = await self.bot._config.locale() - i18n.set_contextual_locale(global_locale) - await self.bot._i18n_cache.set_locale(ctx.guild, None) - await ctx.send(_("Locale has been set to the default.")) - return - try: - locale = BabelLocale.parse(language_code, sep="-") - except (ValueError, UnknownLocaleError): - await ctx.send(_("Invalid language code. Use format: `en-US`")) - return - if locale.territory is None: - await ctx.send( - _("Invalid format - language code has to include country code, e.g. `en-US`") - ) - return - standardized_locale_name = f"{locale.language}-{locale.territory}" - i18n.set_contextual_locale(standardized_locale_name) - await self.bot._i18n_cache.set_locale(ctx.guild, standardized_locale_name) - await ctx.send(_("Locale has been set.")) - - @_set.command(aliases=["globalregion"]) - @commands.guild_only() - @checks.is_owner() - async def globalregionalformat(self, ctx: commands.Context, language_code: str = None): - """ - Changes the bot's regional format. This is used for formatting date, time and numbers. - - `language_code` can be any language code with country code included, e.g. `en-US`, `de-DE`, `fr-FR`, `pl-PL`, etc. - Leave `language_code` empty to base regional formatting on bot's locale. - - **Examples:** - - `[p]set globalregionalformat en-US` - - `[p]set globalregion de-DE` - - `[p]set globalregionalformat` - Resets to the locale. - - **Arguments:** - - `[language_code]` - The default region format to use for the bot. - """ - if language_code is None: - i18n.set_regional_format(None) - await self.bot._i18n_cache.set_regional_format(None, None) - await ctx.send(_("Global regional formatting will now be based on bot's locale.")) - return - - try: - locale = BabelLocale.parse(language_code, sep="-") - except (ValueError, UnknownLocaleError): - await ctx.send(_("Invalid language code. Use format: `en-US`")) - return - if locale.territory is None: - await ctx.send( - _("Invalid format - language code has to include country code, e.g. `en-US`") - ) - return - standardized_locale_name = f"{locale.language}-{locale.territory}" - i18n.set_regional_format(standardized_locale_name) - await self.bot._i18n_cache.set_regional_format(None, standardized_locale_name) - await ctx.send( - _("Global regional formatting will now be based on `{language_code}` locale.").format( - language_code=standardized_locale_name - ) - ) - - @_set.command(aliases=["region"]) - @checks.guildowner_or_permissions(manage_guild=True) - async def regionalformat(self, ctx: commands.Context, language_code: str = None): - """ - Changes the bot's regional format in this server. This is used for formatting date, time and numbers. - - `language_code` can be any language code with country code included, e.g. `en-US`, `de-DE`, `fr-FR`, `pl-PL`, etc. - Leave `language_code` empty to base regional formatting on bot's locale in this server. - - **Examples:** - - `[p]set regionalformat en-US` - - `[p]set region de-DE` - - `[p]set regionalformat` - Resets to the locale. - - **Arguments:** - - `[language_code]` - The region format to use for the bot in this server. - """ - if language_code is None: - i18n.set_contextual_regional_format(None) - await self.bot._i18n_cache.set_regional_format(ctx.guild, None) - await ctx.send( - _("Regional formatting will now be based on bot's locale in this server.") - ) - return - - try: - locale = BabelLocale.parse(language_code, sep="-") - except (ValueError, UnknownLocaleError): - await ctx.send(_("Invalid language code. Use format: `en-US`")) - return - if locale.territory is None: - await ctx.send( - _("Invalid format - language code has to include country code, e.g. `en-US`") - ) - return - standardized_locale_name = f"{locale.language}-{locale.territory}" - i18n.set_contextual_regional_format(standardized_locale_name) - await self.bot._i18n_cache.set_regional_format(ctx.guild, standardized_locale_name) - await ctx.send( - _("Regional formatting will now be based on `{language_code}` locale.").format( - language_code=standardized_locale_name - ) - ) - - @_set.command() - @checks.is_owner() - async def custominfo(self, ctx: commands.Context, *, text: str = None): - """Customizes a section of `[p]info`. - - The maximum amount of allowed characters is 1024. - Supports markdown, links and "mentions". - - Link example: `[My link](https://example.com)` - - **Examples:** - - `[p]set custominfo >>> I can use **markdown** such as quotes, ||spoilers|| and multiple lines.` - - `[p]set custominfo Join my [support server](discord.gg/discord)!` - - `[p]set custominfo` - Removes custom info text. - - **Arguments:** - - `[text]` - The custom info text. - """ - if not text: - await ctx.bot._config.custom_info.clear() - await ctx.send(_("The custom text has been cleared.")) - return - if len(text) <= 1024: - await ctx.bot._config.custom_info.set(text) - await ctx.send(_("The custom text has been set.")) - await ctx.invoke(self.info) - else: - await ctx.send(_("Text must be fewer than 1024 characters long.")) - - @_set.group(invoke_without_command=True) - @checks.is_owner() - async def api(self, ctx: commands.Context, service: str, *, tokens: TokenConverter): - """ - Commands to set, list or remove various external API tokens. - - This setting will be asked for by some 3rd party cogs and some core cogs. - - To add the keys provide the service name and the tokens as a comma separated - list of key,values as described by the cog requesting this command. - - Note: API tokens are sensitive, so this command should only be used in a private channel or in DM with the bot. - - **Examples:** - - `[p]set api Spotify redirect_uri localhost` - - `[p]set api github client_id,whoops client_secret,whoops` - - **Arguments:** - - `` - The service you're adding tokens to. - - `` - Pairs of token keys and values. The key and value should be separated by one of ` `, `,`, or `;`. - """ - if ctx.channel.permissions_for(ctx.me).manage_messages: - await ctx.message.delete() - await ctx.bot.set_shared_api_tokens(service, **tokens) - await ctx.send(_("`{service}` API tokens have been set.").format(service=service)) - - @api.command(name="list") - async def api_list(self, ctx: commands.Context): - """ - Show all external API services along with their keys that have been set. - - Secrets are not shown. - - **Example:** - - `[p]set api list`` - """ - - services: dict = await ctx.bot.get_shared_api_tokens() - if not services: - await ctx.send(_("No API services have been set yet.")) - return - - sorted_services = sorted(services.keys(), key=str.lower) - - joined = _("Set API services:\n") if len(services) > 1 else _("Set API service:\n") - for service_name in sorted_services: - joined += "+ {}\n".format(service_name) - for key_name in services[service_name].keys(): - joined += " - {}\n".format(key_name) - for page in pagify(joined, ["\n"], shorten_by=16): - await ctx.send(box(page.lstrip(" "), lang="diff")) - - @api.command(name="remove", require_var_positional=True) - async def api_remove(self, ctx: commands.Context, *services: str): - """ - Remove the given services with all their keys and tokens. - - **Examples:** - - `[p]set api remove Spotify` - - `[p]set api remove github audiodb` - - **Arguments:** - - `` - The services to remove.""" - bot_services = (await ctx.bot.get_shared_api_tokens()).keys() - services = [s for s in services if s in bot_services] - - if services: - await self.bot.remove_shared_api_services(*services) - if len(services) > 1: - msg = _("Services deleted successfully:\n{services_list}").format( - services_list=humanize_list(services) - ) - else: - msg = _("Service deleted successfully: {service_name}").format( - service_name=services[0] - ) - await ctx.send(msg) - else: - await ctx.send(_("None of the services you provided had any keys set.")) - @commands.group() @checks.is_owner() async def helpset(self, ctx: commands.Context): @@ -4972,133 +5190,6 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic): else: await ctx.send(_("They are not immune.")) - @checks.is_owner() - @_set.group() - async def ownernotifications(self, ctx: commands.Context): - """ - Commands for configuring owner notifications. - - Owner notifications include usage of `[p]contact` and available Red updates. - """ - pass - - @ownernotifications.command() - async def optin(self, ctx: commands.Context): - """ - Opt-in on receiving owner notifications. - - This is the default state. - - Note: This will only resume sending owner notifications to your DMs. - Additional owners and destinations will not be affected. - - **Example:** - - `[p]ownernotifications optin` - """ - async with ctx.bot._config.owner_opt_out_list() as opt_outs: - if ctx.author.id in opt_outs: - opt_outs.remove(ctx.author.id) - - await ctx.tick() - - @ownernotifications.command() - async def optout(self, ctx: commands.Context): - """ - Opt-out of receiving owner notifications. - - Note: This will only stop sending owner notifications to your DMs. - Additional owners and destinations will still receive notifications. - - **Example:** - - `[p]ownernotifications optout` - """ - async with ctx.bot._config.owner_opt_out_list() as opt_outs: - if ctx.author.id not in opt_outs: - opt_outs.append(ctx.author.id) - - await ctx.tick() - - @ownernotifications.command() - async def adddestination( - self, ctx: commands.Context, *, channel: Union[discord.TextChannel, int] - ): - """ - Adds a destination text channel to receive owner notifications. - - **Examples:** - - `[p]ownernotifications adddestination #owner-notifications` - - `[p]ownernotifications adddestination 168091848718417920` - Accepts channel IDs. - - **Arguments:** - - `` - The channel to send owner notifications to. - """ - - try: - channel_id = channel.id - except AttributeError: - channel_id = channel - - async with ctx.bot._config.extra_owner_destinations() as extras: - if channel_id not in extras: - extras.append(channel_id) - - await ctx.tick() - - @ownernotifications.command(aliases=["remdestination", "deletedestination", "deldestination"]) - async def removedestination( - self, ctx: commands.Context, *, channel: Union[discord.TextChannel, int] - ): - """ - Removes a destination text channel from receiving owner notifications. - - **Examples:** - - `[p]ownernotifications removedestination #owner-notifications` - - `[p]ownernotifications deletedestination 168091848718417920` - Accepts channel IDs. - - **Arguments:** - - `` - The channel to stop sending owner notifications to. - """ - - try: - channel_id = channel.id - except AttributeError: - channel_id = channel - - async with ctx.bot._config.extra_owner_destinations() as extras: - if channel_id in extras: - extras.remove(channel_id) - - await ctx.tick() - - @ownernotifications.command() - async def listdestinations(self, ctx: commands.Context): - """ - Lists the configured extra destinations for owner notifications. - - **Example:** - - `[p]ownernotifications listdestinations` - """ - - channel_ids = await ctx.bot._config.extra_owner_destinations() - - if not channel_ids: - await ctx.send(_("There are no extra channels being sent to.")) - return - - data = [] - - for channel_id in channel_ids: - channel = ctx.bot.get_channel(channel_id) - if channel: - # This includes the channel name in case the user can't see the channel. - data.append(f"{channel.mention} {channel} ({channel.id})") - else: - data.append(_("Unknown channel with id: {id}").format(id=channel_id)) - - output = "\n".join(data) - for page in pagify(output): - await ctx.send(page) - # RPC handlers async def rpc_load(self, request): cog_name = request.params[0]