diff --git a/adminutils/__init__.py b/adminutils/__init__.py index 347f9d29..ac853a4c 100644 --- a/adminutils/__init__.py +++ b/adminutils/__init__.py @@ -17,8 +17,8 @@ async def setup_after_ready(bot): for alias in command.aliases: if bot.get_command(alias): command.aliases[command.aliases.index(alias)] = f"a{alias}" - bot.add_cog(cog) + await bot.add_cog(cog) -def setup(bot): +async def setup(bot): create_task(setup_after_ready(bot)) diff --git a/adminutils/adminutils.py b/adminutils/adminutils.py index b905eb95..ea22bbe2 100644 --- a/adminutils/adminutils.py +++ b/adminutils/adminutils.py @@ -1,3 +1,4 @@ +import contextlib import re from asyncio import TimeoutError as AsyncTimeoutError from random import choice @@ -5,35 +6,55 @@ import aiohttp import discord +from red_commons.logging import getLogger from redbot.core import commands from redbot.core.i18n import Translator, cog_i18n from redbot.core.utils import chat_formatting as chat from redbot.core.utils.mod import get_audit_reason from redbot.core.utils.predicates import MessagePredicate -try: - from redbot import json # support of Draper's branch -except ImportError: - import json - _ = Translator("AdminUtils", __file__) EMOJI_RE = re.compile(r"(<(a)?:[a-zA-Z0-9_]+:([0-9]+)>)") +CHANNEL_REASONS = { + discord.CategoryChannel: _("You are not allowed to edit this category."), + discord.TextChannel: _("You are not allowed to edit this channel."), + discord.VoiceChannel: _("You are not allowed to edit this channel."), + discord.StageChannel: _("You are not allowed to edit this channel."), +} + + +async def check_regions(ctx): + """Check if regions list is populated""" + return ctx.cog.regions + @cog_i18n(_) class AdminUtils(commands.Cog): """Useful commands for server administrators.""" - __version__ = "2.5.11" + __version__ = "3.0.1-pre" # noinspection PyMissingConstructor def __init__(self, bot): self.bot = bot - self.session = aiohttp.ClientSession(json_serialize=json.dumps) + self.session = aiohttp.ClientSession() + self.log = getLogger("red.fixator10-cogs.adminutils") + self.regions = [] + + async def cog_load(self): + try: + regions = await self.bot.http.request(discord.http.Route("GET", "/voice/regions")) + self.regions = [region["id"] for region in regions] + except Exception as e: + self.log.warning( + "Unable to get list of rtc_regions. [p]restartvoice command will be unavailable", + exc_info=e, + ) - def cog_unload(self): - self.bot.loop.create_task(self.session.close()) + async def cog_unload(self): + await self.session.close() def format_help_for_context(self, ctx: commands.Context) -> str: # Thanks Sinbad! pre_processed = super().format_help_for_context(ctx) @@ -45,20 +66,19 @@ async def red_delete_data_for_user(self, **kwargs): @staticmethod def check_channel_permission( ctx: commands.Context, - channel_or_category: Union[discord.TextChannel, discord.CategoryChannel], + channel_or_category: Union[ + discord.TextChannel, + discord.CategoryChannel, + discord.VoiceChannel, + discord.StageChannel, + ], ) -> bool: """ Check user's permission in a channel, to be sure he can edit it. """ - mc = channel_or_category.permissions_for(ctx.author).manage_channels - if mc: + if channel_or_category.permissions_for(ctx.author).manage_channels: return True - reason = ( - _("You are not allowed to edit this channel.") - if not isinstance(channel_or_category, discord.CategoryChannel) - else _("You are not allowed to edit in this category.") - ) - raise commands.UserFeedbackCheckFailure(reason) + raise commands.UserFeedbackCheckFailure(CHANNEL_REASONS.get(type(channel_or_category))) @commands.command(name="prune") @commands.guild_only() @@ -92,10 +112,8 @@ async def cleanup_users(self, ctx, days: Optional[int] = 1, *roles: discord.Role ).format(to_kick=to_kick, days=days, roles=roles_text if roles else "") ) ) - try: + with contextlib.suppress(AsyncTimeoutError): await self.bot.wait_for("message", check=pred, timeout=30) - except AsyncTimeoutError: - pass if ctx.assume_yes or pred.result: cleanup = await ctx.guild.prune_members( days=days, reason=get_audit_reason(ctx.author), roles=roles or None @@ -113,23 +131,20 @@ async def cleanup_users(self, ctx, days: Optional[int] = 1, *roles: discord.Role @commands.command() @commands.guild_only() - @commands.admin_or_permissions(manage_guild=True) - @commands.bot_has_permissions(manage_guild=True) - async def restartvoice(self, ctx: commands.Context): - """Change server's voice region to random and back + @commands.check(check_regions) + @commands.admin_or_permissions(manage_channels=True) + @commands.bot_has_permissions(manage_channels=True) + async def restartvoice( + self, ctx: commands.Context, channel: Union[discord.VoiceChannel, discord.StageChannel] + ): + """Change voice channel's region to random and back Useful to reinitate all voice connections""" - current_region = ctx.guild.region - random_region = choice( - [ - r - for r in discord.VoiceRegion - if not r.value.startswith("vip") and current_region != r - ] - ) - await ctx.guild.edit(region=random_region) - await ctx.guild.edit( - region=current_region, + current_region = channel.rtc_region + random_region = choice([r for r in self.regions if current_region != r]) + await channel.edit(rtc_region=random_region) + await channel.edit( + rtc_region=current_region, reason=get_audit_reason(ctx.author, _("Voice restart")), ) await ctx.tick() @@ -142,8 +157,8 @@ async def restartvoice(self, ctx: commands.Context): async def massmove( self, ctx: commands.Context, - from_channel: discord.VoiceChannel, - to_channel: discord.VoiceChannel = None, + from_channel: Union[discord.VoiceChannel, discord.StageChannel], + to_channel: Union[discord.VoiceChannel, discord.StageChannel] = None, ): """Move all members from one voice channel to another @@ -171,6 +186,7 @@ async def massmove( continue await ctx.send(_("Finished moving users. {} members could not be moved.").format(fails)) + # TODO: Stickers? @commands.group() @commands.guild_only() @commands.admin_or_permissions(manage_emojis=True) @@ -209,8 +225,6 @@ async def emoji_add(self, ctx, name: str, url: str, *roles: discord.Role): ), ), ) - except discord.InvalidArgument: - await ctx.send(chat.error(_("This image type is unsupported, or link is incorrect"))) except discord.HTTPException as e: await ctx.send(chat.error(_("An error occurred on adding an emoji: {}").format(e))) else: @@ -255,13 +269,6 @@ async def emote_steal( ), ) await ctx.tick() - except discord.InvalidArgument: - await ctx.send( - _( - "This image type is not supported anymore or Discord returned incorrect data. Try again later." - ) - ) - return except discord.HTTPException as e: await ctx.send(chat.error(_("An error occurred on adding an emoji: {}").format(e))) @@ -307,6 +314,7 @@ async def emoji_remove(self, ctx: commands.Context, *, emoji: discord.Emoji): await emoji.delete(reason=get_audit_reason(ctx.author)) await ctx.tick() + # TODO: Threads? @commands.group() @commands.guild_only() @commands.admin_or_permissions(manage_channels=True) @@ -366,8 +374,8 @@ async def channel_create_voice( Use double quotes if category has spaces Examples: - `[p]channel add voice "The Zoo" Awesome Channel` will create under the "The Zoo" category. - `[p]channel add voice Awesome Channel` will create under no category, at the top. + `[p]channel add voice "The Zoo" Awesome Channel` will create voice channel under the "The Zoo" category. + `[p]channel add voice Awesome Channel` will create stage channel under no category, at the top. """ if category: self.check_channel_permission(ctx, category) @@ -382,11 +390,41 @@ async def channel_create_voice( else: await ctx.tick() + @channel_create.command(name="stage") + async def channel_create_stage( + self, + ctx: commands.Context, + category: Optional[discord.CategoryChannel] = None, + *, + name: str, + ): + """Create a stage channel + + You can create the channel under a category if passed, else it is created under no category + Use double quotes if category has spaces + + Examples: + `[p]channel add voice "The Zoo" Awesome Channel` will create voice channel under the "The Zoo" category. + `[p]channel add voice Awesome Channel` will create stage channel under no category, at the top. + """ + if category: + self.check_channel_permission(ctx, category) + try: + await ctx.guild.create_stage_channel( + name, category=category, reason=get_audit_reason(ctx.author) + ) + except discord.Forbidden: + await ctx.send(chat.error(_("I can't create channel in this category"))) + except discord.HTTPException as e: + await ctx.send(chat.error(_("I am unable to create a channel: {}").format(e))) + else: + await ctx.tick() + @channel.command(name="rename") async def channel_rename( self, ctx: commands.Context, - channel: Union[discord.TextChannel, discord.VoiceChannel], + channel: Union[discord.TextChannel, discord.VoiceChannel, discord.StageChannel], *, name: str, ): @@ -409,7 +447,10 @@ async def channel_rename( @channel.command(name="delete", aliases=["remove"]) async def channel_delete( - self, ctx: commands.Context, *, channel: Union[discord.TextChannel, discord.VoiceChannel] + self, + ctx: commands.Context, + *, + channel: Union[discord.TextChannel, discord.VoiceChannel, discord.StageChannel], ): """Remove a channel from server @@ -427,10 +468,8 @@ async def channel_delete( ).format(channel=channel.mention) ) ) - try: + with contextlib.suppress(AsyncTimeoutError): await self.bot.wait_for("message", check=pred, timeout=30) - except AsyncTimeoutError: - pass if ctx.assume_yes or pred.result: try: await channel.delete(reason=get_audit_reason(ctx.author)) diff --git a/adminutils/info.json b/adminutils/info.json index 668f7026..1779d7d7 100644 --- a/adminutils/info.json +++ b/adminutils/info.json @@ -8,8 +8,7 @@ "install_msg": "Thanks for install.\nThis cog contains some useful commands for server administrators.", "short": "Useful commands for server administrators.", "description": "Useful commands for server administrators.", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.99", + "min_bot_version": "3.5.0", "tags": [ "admin", "emoji", diff --git a/adminutils/locales/messages.pot b/adminutils/locales/messages.pot index aa3374b8..eede162d 100644 --- a/adminutils/locales/messages.pot +++ b/adminutils/locales/messages.pot @@ -2,77 +2,78 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2022-10-01 20:26+0400\n" +"POT-Creation-Date: 2023-06-01 17:37+0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: redgettext 3.3\n" +"Generated-By: redgettext 3.4.2\n" -#: adminutils\adminutils.py:26 -#, docstring -msgid "Useful commands for server administrators." +#: adminutils\adminutils.py:21 +msgid "You are not allowed to edit this category." msgstr "" -#: adminutils\adminutils.py:57 +#: adminutils\adminutils.py:22 adminutils\adminutils.py:23 +#: adminutils\adminutils.py:24 msgid "You are not allowed to edit this channel." msgstr "" -#: adminutils\adminutils.py:59 -msgid "You are not allowed to edit in this category." +#: adminutils\adminutils.py:35 +#, docstring +msgid "Useful commands for server administrators." msgstr "" -#: adminutils\adminutils.py:68 +#: adminutils\adminutils.py:88 #, docstring msgid "Cleanup inactive server members" msgstr "" -#: adminutils\adminutils.py:72 +#: adminutils\adminutils.py:92 msgid "" "Due to Discord Restrictions, you cannot use more than 30 days for that cmd." msgstr "" -#: adminutils\adminutils.py:79 +#: adminutils\adminutils.py:99 msgid "\"days\" arg cannot be less than 1..." msgstr "" -#: adminutils\adminutils.py:84 +#: adminutils\adminutils.py:104 msgid "" "\n" "Including members in roles: {}\n" msgstr "" -#: adminutils\adminutils.py:89 +#: adminutils\adminutils.py:109 msgid "" "You are about to kick **{to_kick}** inactive for **{days}** days members from this server. {roles}Are you sure?\n" "To agree, type \"yes\"" msgstr "" -#: adminutils\adminutils.py:105 +#: adminutils\adminutils.py:123 msgid "" "**{removed}**/**{all}** inactive members removed.\n" "(They were inactive for **{days}** days)" msgstr "" -#: adminutils\adminutils.py:112 +#: adminutils\adminutils.py:130 msgid "Inactive members cleanup canceled." msgstr "" -#: adminutils\adminutils.py:119 +#: adminutils\adminutils.py:140 #, docstring msgid "" -"Change server's voice region to random and back\n" +"Change voice channel's region to random and back\n" "\n" " Useful to reinitate all voice connections" msgstr "" -#: adminutils\adminutils.py:133 +#: adminutils\adminutils.py:148 msgid "Voice restart" msgstr "" -#: adminutils\adminutils.py:148 +#: adminutils\adminutils.py:163 #, docstring msgid "" "Move all members from one voice channel to another\n" @@ -80,32 +81,32 @@ msgid "" " Use double quotes if channel name has spaces" msgstr "" -#: adminutils\adminutils.py:154 +#: adminutils\adminutils.py:169 msgid "There is no users in channel {}." msgstr "" -#: adminutils\adminutils.py:158 +#: adminutils\adminutils.py:173 msgid "I cant move users from that channel" msgstr "" -#: adminutils\adminutils.py:161 +#: adminutils\adminutils.py:176 msgid "I cant move users to that channel" msgstr "" -#: adminutils\adminutils.py:167 +#: adminutils\adminutils.py:182 msgid "Massmove" msgstr "" -#: adminutils\adminutils.py:172 +#: adminutils\adminutils.py:187 msgid "Finished moving users. {} members could not be moved." msgstr "" -#: adminutils\adminutils.py:179 +#: adminutils\adminutils.py:195 #, docstring msgid "Manage emoji" msgstr "" -#: adminutils\adminutils.py:184 +#: adminutils\adminutils.py:200 #, docstring msgid "" "Create custom emoji\n" @@ -118,23 +119,19 @@ msgid "" " " msgstr "" -#: adminutils\adminutils.py:196 +#: adminutils\adminutils.py:212 msgid "Unable to get emoji from provided url: {}" msgstr "" -#: adminutils\adminutils.py:205 adminutils\adminutils.py:248 +#: adminutils\adminutils.py:221 adminutils\adminutils.py:262 msgid "Restricted to roles: {}" msgstr "" -#: adminutils\adminutils.py:211 -msgid "This image type is unsupported, or link is incorrect" -msgstr "" - -#: adminutils\adminutils.py:213 adminutils\adminutils.py:262 +#: adminutils\adminutils.py:227 adminutils\adminutils.py:269 msgid "An error occurred on adding an emoji: {}" msgstr "" -#: adminutils\adminutils.py:221 +#: adminutils\adminutils.py:235 #, docstring msgid "" "\n" @@ -147,17 +144,11 @@ msgid "" " " msgstr "" -#: adminutils\adminutils.py:233 +#: adminutils\adminutils.py:247 msgid "No emojis found specified message." msgstr "" -#: adminutils\adminutils.py:256 -msgid "" -"This image type is not supported anymore or Discord returned incorrect data." -" Try again later." -msgstr "" - -#: adminutils\adminutils.py:268 +#: adminutils\adminutils.py:275 #, docstring msgid "" "Rename emoji and restrict to certain roles\n" @@ -171,30 +162,30 @@ msgid "" " " msgstr "" -#: adminutils\adminutils.py:286 +#: adminutils\adminutils.py:293 msgid "Restricted to roles: " msgstr "" -#: adminutils\adminutils.py:292 +#: adminutils\adminutils.py:299 msgid "I can't edit this emoji" msgstr "" -#: adminutils\adminutils.py:297 +#: adminutils\adminutils.py:304 #, docstring msgid "Remove emoji from server" msgstr "" -#: adminutils\adminutils.py:309 +#: adminutils\adminutils.py:317 #, docstring msgid "Manage channels" msgstr "" -#: adminutils\adminutils.py:317 +#: adminutils\adminutils.py:325 #, docstring msgid "Create a channel" msgstr "" -#: adminutils\adminutils.py:327 +#: adminutils\adminutils.py:335 #, docstring msgid "" "Create a text channel\n" @@ -208,15 +199,17 @@ msgid "" " " msgstr "" -#: adminutils\adminutils.py:343 adminutils\adminutils.py:373 +#: adminutils\adminutils.py:351 adminutils\adminutils.py:381 +#: adminutils\adminutils.py:411 msgid "I can't create channel in this category" msgstr "" -#: adminutils\adminutils.py:345 adminutils\adminutils.py:375 +#: adminutils\adminutils.py:353 adminutils\adminutils.py:383 +#: adminutils\adminutils.py:413 msgid "I am unable to create a channel: {}" msgstr "" -#: adminutils\adminutils.py:357 +#: adminutils\adminutils.py:365 #, docstring msgid "" "Create a voice channel\n" @@ -225,12 +218,26 @@ msgid "" " Use double quotes if category has spaces\n" "\n" " Examples:\n" -" `[p]channel add voice \"The Zoo\" Awesome Channel` will create under the \"The Zoo\" category.\n" -" `[p]channel add voice Awesome Channel` will create under no category, at the top.\n" +" `[p]channel add voice \"The Zoo\" Awesome Channel` will create voice channel under the \"The Zoo\" category.\n" +" `[p]channel add voice Awesome Channel` will create stage channel under no category, at the top.\n" +" " +msgstr "" + +#: adminutils\adminutils.py:395 +#, docstring +msgid "" +"Create a stage channel\n" +"\n" +" You can create the channel under a category if passed, else it is created under no category\n" +" Use double quotes if category has spaces\n" +"\n" +" Examples:\n" +" `[p]channel add voice \"The Zoo\" Awesome Channel` will create voice channel under the \"The Zoo\" category.\n" +" `[p]channel add voice Awesome Channel` will create stage channel under no category, at the top.\n" " " msgstr "" -#: adminutils\adminutils.py:387 +#: adminutils\adminutils.py:425 #, docstring msgid "" "Rename a channel\n" @@ -242,15 +249,15 @@ msgid "" " " msgstr "" -#: adminutils\adminutils.py:398 +#: adminutils\adminutils.py:436 msgid "I can't rename this channel" msgstr "" -#: adminutils\adminutils.py:400 +#: adminutils\adminutils.py:438 msgid "I am unable to rename this channel: {}" msgstr "" -#: adminutils\adminutils.py:408 +#: adminutils\adminutils.py:449 #, docstring msgid "" "Remove a channel from server\n" @@ -260,16 +267,16 @@ msgid "" " " msgstr "" -#: adminutils\adminutils.py:418 +#: adminutils\adminutils.py:459 msgid "" "You are about to delete channel {channel}. This cannot be undone. Are you sure?\n" "To agree, type \"yes\"" msgstr "" -#: adminutils\adminutils.py:432 +#: adminutils\adminutils.py:471 msgid "I can't delete this channel" msgstr "" -#: adminutils\adminutils.py:434 +#: adminutils\adminutils.py:473 msgid "I am unable to delete a channel: {}" msgstr "" diff --git a/adminutils/locales/ru-RU.po b/adminutils/locales/ru-RU.po index d786944f..960f8b66 100644 --- a/adminutils/locales/ru-RU.po +++ b/adminutils/locales/ru-RU.po @@ -1,292 +1,392 @@ msgid "" msgstr "" "Project-Id-Version: fixator10-cogs\n" -"POT-Creation-Date: 2022-10-01 20:26+0400\n" +"POT-Creation-Date: 2023-06-01 17:37+0400\n" +"PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Russian\n" +"Language: ru_RU\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 " +"&& n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 " +"&& n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" "Generated-By: redgettext 3.3\n" -"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" "X-Crowdin-Project: fixator10-cogs\n" "X-Crowdin-Project-ID: 387695\n" "X-Crowdin-Language: ru\n" "X-Crowdin-File-ID: 57\n" -"Language: ru_RU\n" +"X-Generator: Poedit 3.2.2\n" -#: adminutils\adminutils.py:26 -#, docstring -msgid "Useful commands for server administrators." -msgstr "Полезные команды для администраторов серверов." +#: adminutils\adminutils.py:21 +msgid "You are not allowed to edit this category." +msgstr "Вы не можете редактировать эту категорию." -#: adminutils\adminutils.py:57 +#: adminutils\adminutils.py:22 adminutils\adminutils.py:23 +#: adminutils\adminutils.py:24 msgid "You are not allowed to edit this channel." msgstr "Вы не можете редактировать этот канал." -#: adminutils\adminutils.py:59 -msgid "You are not allowed to edit in this category." -msgstr "Вы не можете редактировать каналы в этой категории." +#: adminutils\adminutils.py:35 +msgid "Useful commands for server administrators." +msgstr "Полезные команды для администраторов серверов." -#: adminutils\adminutils.py:68 -#, docstring +#: adminutils\adminutils.py:88 msgid "Cleanup inactive server members" msgstr "Чистка неактивных участников сервера" -#: adminutils\adminutils.py:72 -msgid "Due to Discord Restrictions, you cannot use more than 30 days for that cmd." -msgstr "В связи с ограничениями Discord, вы не можете использовать более 30 дней для этой команды." +#: adminutils\adminutils.py:92 +msgid "" +"Due to Discord Restrictions, you cannot use more than 30 days for that cmd." +msgstr "" +"В связи с ограничениями Discord, вы не можете использовать более 30 дней для " +"этой команды." -#: adminutils\adminutils.py:79 +#: adminutils\adminutils.py:99 msgid "\"days\" arg cannot be less than 1..." msgstr "Аргумент \"days\" не может быть меньше 1..." -#: adminutils\adminutils.py:84 -msgid "\n" +#: adminutils\adminutils.py:104 +msgid "" +"\n" "Including members in roles: {}\n" -msgstr "\n" +msgstr "" +"\n" "Включая участников в ролях: {}\n" -#: adminutils\adminutils.py:89 -msgid "You are about to kick **{to_kick}** inactive for **{days}** days members from this server. {roles}Are you sure?\n" +#: adminutils\adminutils.py:109 +msgid "" +"You are about to kick **{to_kick}** inactive for **{days}** days members " +"from this server. {roles}Are you sure?\n" "To agree, type \"yes\"" -msgstr "Вы собираетесь выгнать **{to_kick}** пользователей, бывших неактивными **{days}** дней с этого сервера. {roles}Вы уверены в этом?\n" +msgstr "" +"Вы собираетесь выгнать **{to_kick}** пользователей, бывших неактивными " +"**{days}** дней с этого сервера. {roles}Вы уверены в этом?\n" "Для продолжения напишите \"yes\"" -#: adminutils\adminutils.py:105 -msgid "**{removed}**/**{all}** inactive members removed.\n" +#: adminutils\adminutils.py:123 +msgid "" +"**{removed}**/**{all}** inactive members removed.\n" "(They were inactive for **{days}** days)" -msgstr "**{removed}**/**{all}** неактивных участников удалено.\n" +msgstr "" +"**{removed}**/**{all}** неактивных участников удалено.\n" "(Они были неактивны **{days}** дней)" -#: adminutils\adminutils.py:112 +#: adminutils\adminutils.py:130 msgid "Inactive members cleanup canceled." msgstr "Очистка неактивных участников отменена." -#: adminutils\adminutils.py:119 -#, docstring -msgid "Change server's voice region to random and back\n\n" +#: adminutils\adminutils.py:140 +msgid "" +"Change voice channel's region to random and back\n" +"\n" " Useful to reinitate all voice connections" -msgstr "Изменить голосовой регион сервера на случайный и обратно\n\n" +msgstr "" +"Изменить голосовой регион канала на случайный и обратно\n" +"\n" " Полезно для восстановления всех голосовых подключений" -#: adminutils\adminutils.py:133 +#: adminutils\adminutils.py:148 msgid "Voice restart" msgstr "Перезапуск голосового чата" -#: adminutils\adminutils.py:148 -#, docstring -msgid "Move all members from one voice channel to another\n\n" +#: adminutils\adminutils.py:163 +msgid "" +"Move all members from one voice channel to another\n" +"\n" " Use double quotes if channel name has spaces" -msgstr "Переместить всех участников с одного голосового канала в другой\n\n" +msgstr "" +"Переместить всех участников с одного голосового канала в другой\n" +"\n" " Используйте двойные кавычки, если название канала содержит пробелы" -#: adminutils\adminutils.py:154 +#: adminutils\adminutils.py:169 msgid "There is no users in channel {}." msgstr "Нет пользователей в канале {}." -#: adminutils\adminutils.py:158 +#: adminutils\adminutils.py:173 msgid "I cant move users from that channel" msgstr "Я не могу двигать пользователей из этого канала" -#: adminutils\adminutils.py:161 +#: adminutils\adminutils.py:176 msgid "I cant move users to that channel" msgstr "Я не могу двигать пользователей в этот канал" -#: adminutils\adminutils.py:167 +#: adminutils\adminutils.py:182 msgid "Massmove" msgstr "Массовое перемещение" -#: adminutils\adminutils.py:172 +#: adminutils\adminutils.py:187 msgid "Finished moving users. {} members could not be moved." -msgstr "Перемещение пользователей завершено. {} участников не удалось переместить." +msgstr "" +"Перемещение пользователей завершено. {} участников не удалось переместить." -#: adminutils\adminutils.py:179 -#, docstring +#: adminutils\adminutils.py:195 msgid "Manage emoji" msgstr "Управление эмодзи" -#: adminutils\adminutils.py:184 -#, docstring -msgid "Create custom emoji\n\n" -" Use double quotes if role name has spaces\n\n" +#: adminutils\adminutils.py:200 +msgid "" +"Create custom emoji\n" +"\n" +" Use double quotes if role name has spaces\n" +"\n" " Examples:\n" " `[p]emoji add Example https://example.com/image.png`\n" -" `[p]emoji add RoleBased https://example.com/image.png EmojiRole \"Test image\"`\n" +" `[p]emoji add RoleBased https://example.com/image.png EmojiRole " +"\"Test image\"`\n" " " -msgstr "Создать собственное эмодзи\n\n" -" Используйте двойные кавычки, если название роли содержит пробелы\n\n" +msgstr "" +"Создать собственное эмодзи\n" +"\n" +" Используйте двойные кавычки, если название роли содержит пробелы\n" +"\n" " Примеры:\n" " `[p]emoji add Example https://example.com/image.png`\n" -" `[p]emoji add RoleBased https://example.com/image.png РольДляЭмодзи \"Тестовое изображение\"`\n" +" `[p]emoji add RoleBased https://example.com/image.png " +"РольДляЭмодзи \"Тестовое изображение\"`\n" " " -#: adminutils\adminutils.py:196 +#: adminutils\adminutils.py:212 msgid "Unable to get emoji from provided url: {}" msgstr "Невозможно получить эмодзи по предоставленному адресу: {}" -#: adminutils\adminutils.py:205 adminutils\adminutils.py:248 +#: adminutils\adminutils.py:221 adminutils\adminutils.py:262 msgid "Restricted to roles: {}" msgstr "Ограничено для ролей: {}" -#: adminutils\adminutils.py:211 -msgid "This image type is unsupported, or link is incorrect" -msgstr "Данный тип изображений не поддерживается, или указана неверная ссылка" - -#: adminutils\adminutils.py:213 adminutils\adminutils.py:262 +#: adminutils\adminutils.py:227 adminutils\adminutils.py:269 msgid "An error occurred on adding an emoji: {}" msgstr "Произошла ошибка при добавлении эмодзи: {}" -#: adminutils\adminutils.py:221 -#, docstring -msgid "\n" +#: adminutils\adminutils.py:235 +msgid "" +"\n" " Add an emoji from a specified message\n" -" Use double quotes if role name has spaces\n\n" +" Use double quotes if role name has spaces\n" +"\n" " Examples:\n" " `[p]emoji message Example 162379234070467641`\n" " `[p]emoji message RoleBased 162379234070467641 EmojiRole`\n" " " -msgstr "\n" +msgstr "" +"\n" " Добавить эмодзи из указанного сообщения\n" -" Используйте двойные кавычки, если название роли содержит пробелы\n\n" +" Используйте двойные кавычки, если название роли содержит пробелы\n" +"\n" " Примеры:\n" " `[p]emoji message Example 162379234070467641`\n" " `[p]emoji message RoleBased 162379234070467641 РольДляЭмодзи`\n" " " -#: adminutils\adminutils.py:233 +#: adminutils\adminutils.py:247 msgid "No emojis found specified message." msgstr "В указанном сообщении не найдено эмодзи." -#: adminutils\adminutils.py:256 -msgid "This image type is not supported anymore or Discord returned incorrect data. Try again later." -msgstr "Данный тип изображения больше не поддерживается, или Discord вернул неверную информацию. Попробуйте позже." - -#: adminutils\adminutils.py:268 -#, docstring -msgid "Rename emoji and restrict to certain roles\n" -" Only this roles will be able to use this emoji\n\n" -" Use double quotes if role name has spaces\n\n" +#: adminutils\adminutils.py:275 +msgid "" +"Rename emoji and restrict to certain roles\n" +" Only this roles will be able to use this emoji\n" +"\n" +" Use double quotes if role name has spaces\n" +"\n" " Examples:\n" " `[p]emoji rename emoji NewEmojiName`\n" -" `[p]emoji rename emoji NewEmojiName Administrator \"Allowed role\"`\n" +" `[p]emoji rename emoji NewEmojiName Administrator \"Allowed " +"role\"`\n" " " -msgstr "Переименовать эмодзи и ограничить использование для определённых ролей\n" -" Только выбранные роли смогу использовать это эмодзи\n\n" -" Используйте двойные ковычки если название роли содержит пробелы\n\n" +msgstr "" +"Переименовать эмодзи и ограничить использование для определённых ролей\n" +" Только выбранные роли смогу использовать это эмодзи\n" +"\n" +" Используйте двойные ковычки если название роли содержит пробелы\n" +"\n" " Examples:\n" " `[p]emoji rename emoji NewEmojiName`\n" -" `[p]emoji rename emoji NewEmojiName Администратор \"Допустимая роль\"`\n" +" `[p]emoji rename emoji NewEmojiName Администратор \"Допустимая " +"роль\"`\n" " " -#: adminutils\adminutils.py:286 +#: adminutils\adminutils.py:293 msgid "Restricted to roles: " msgstr "Ограничено для ролей: " -#: adminutils\adminutils.py:292 +#: adminutils\adminutils.py:299 msgid "I can't edit this emoji" msgstr "Я не могу изменить эту эмодзи" -#: adminutils\adminutils.py:297 -#, docstring +#: adminutils\adminutils.py:304 msgid "Remove emoji from server" msgstr "Удалить эмодзи с сервера" -#: adminutils\adminutils.py:309 -#, docstring +#: adminutils\adminutils.py:317 msgid "Manage channels" msgstr "Управление каналами" -#: adminutils\adminutils.py:317 -#, docstring +#: adminutils\adminutils.py:325 msgid "Create a channel" msgstr "Создать канал" -#: adminutils\adminutils.py:327 -#, docstring -msgid "Create a text channel\n\n" -" You can create the channel under a category if passed, else it is created under no category\n" -" Use double quotes if category has spaces\n\n" +#: adminutils\adminutils.py:335 +msgid "" +"Create a text channel\n" +"\n" +" You can create the channel under a category if passed, else it is " +"created under no category\n" +" Use double quotes if category has spaces\n" +"\n" " Examples:\n" -" `[p]channel add text \"The Zoo\" awesome-channel` will create under the \"The Zoo\" category.\n" -" `[p]channel add text awesome-channel` will create under no category, at the top.\n" +" `[p]channel add text \"The Zoo\" awesome-channel` will create " +"under the \"The Zoo\" category.\n" +" `[p]channel add text awesome-channel` will create under no " +"category, at the top.\n" " " -msgstr "Создать текстовый канал\n\n" -" Вы можете создать канал в категории, если укажете её, в противном случае он будет создан вне категории\n" -" Используйте двойные кавычки, если категория содержит пробелы\n\n" +msgstr "" +"Создать текстовый канал\n" +"\n" +" Вы можете создать канал в категории, если укажете её, в противном " +"случае он будет создан вне категории\n" +" Используйте двойные кавычки, если категория содержит пробелы\n" +"\n" " Примеры:\n" -" `[p]channel add text \"Зоопарк\" отличный-канал` создаст канал в категории \"Зоопарк\".\n" -" `[p]channel add text отличный-канал` создаст канал вне категории, сверху.\n" +" `[p]channel add text \"Зоопарк\" отличный-канал` создаст канал в " +"категории \"Зоопарк\".\n" +" `[p]channel add text отличный-канал` создаст канал вне " +"категории, сверху.\n" " " -#: adminutils\adminutils.py:343 adminutils\adminutils.py:373 +#: adminutils\adminutils.py:351 adminutils\adminutils.py:381 +#: adminutils\adminutils.py:411 msgid "I can't create channel in this category" msgstr "Я не могу создать канал в этой категории" -#: adminutils\adminutils.py:345 adminutils\adminutils.py:375 +#: adminutils\adminutils.py:353 adminutils\adminutils.py:383 +#: adminutils\adminutils.py:413 msgid "I am unable to create a channel: {}" msgstr "Невозможно создать канал: {}" -#: adminutils\adminutils.py:357 -#, docstring -msgid "Create a voice channel\n\n" -" You can create the channel under a category if passed, else it is created under no category\n" -" Use double quotes if category has spaces\n\n" +#: adminutils\adminutils.py:365 +msgid "" +"Create a voice channel\n" +"\n" +" You can create the channel under a category if passed, else it is " +"created under no category\n" +" Use double quotes if category has spaces\n" +"\n" +" Examples:\n" +" `[p]channel add voice \"The Zoo\" Awesome Channel` will create " +"voice channel under the \"The Zoo\" category.\n" +" `[p]channel add voice Awesome Channel` will create stage channel " +"under no category, at the top.\n" +" " +msgstr "" +"Создать голосовой канал\n" +"\n" +" Вы можете создать канал в категории, если укажете её, в противном " +"случае он будет создан вне категории\n" +" Используйте двойные кавычки, если категория содержит пробелы\n" +"\n" +" Примеры:\n" +" `[p]channel add text \"Зоо парк\" Отличный канал` создаст канал " +"в категории \"Зоо парк\".\n" +" `[p]channel add text Отличный канал` создаст канал вне " +"категории, сверху.\n" +" " + +#: adminutils\adminutils.py:395 +msgid "" +"Create a stage channel\n" +"\n" +" You can create the channel under a category if passed, else it is " +"created under no category\n" +" Use double quotes if category has spaces\n" +"\n" " Examples:\n" -" `[p]channel add voice \"The Zoo\" Awesome Channel` will create under the \"The Zoo\" category.\n" -" `[p]channel add voice Awesome Channel` will create under no category, at the top.\n" +" `[p]channel add voice \"The Zoo\" Awesome Channel` will create " +"voice channel under the \"The Zoo\" category.\n" +" `[p]channel add voice Awesome Channel` will create stage channel " +"under no category, at the top.\n" " " -msgstr "Создать голосовой канал\n\n" -" Вы можете создать канал в категории, если укажете её, в противном случае он будет создан вне категории\n" -" Используйте двойные кавычки, если категория содержит пробелы\n\n" +msgstr "" +"Создать канал-сцену\n" +"\n" +" Вы можете создать канал в категории, если укажете её, в противном " +"случае он будет создан вне категории\n" +" Используйте двойные кавычки, если категория содержит пробелы\n" +"\n" " Примеры:\n" -" `[p]channel add text \"Зоопарк\" Отличный канал` создаст канал в категории \"Зоопарк\".\n" -" `[p]channel add text Отличный канал` создаст канал вне категории, сверху.\n" +" `[p]channel add text \"Зоо парк\" Отличный канал` создаст канал " +"в категории \"Зоо парк\".\n" +" `[p]channel add text Отличный канал` создаст канал вне " +"категории, сверху.\n" " " -#: adminutils\adminutils.py:387 -#, docstring -msgid "Rename a channel\n\n" -" Use double quotes if channel has spaces\n\n" +#: adminutils\adminutils.py:425 +msgid "" +"Rename a channel\n" +"\n" +" Use double quotes if channel has spaces\n" +"\n" " Examples:\n" " `[p]channel rename channel new-channel-name`\n" " " -msgstr "Переименовать канал\n\n" -" Используйте двойные кавычки, если название канала содержит пробелы\n\n" +msgstr "" +"Переименовать канал\n" +"\n" +" Используйте двойные кавычки, если название канала содержит пробелы\n" +"\n" " Примеры:\n" " `[p]channel rename канал новое-название-канала`\n" " " -#: adminutils\adminutils.py:398 +#: adminutils\adminutils.py:436 msgid "I can't rename this channel" msgstr "Я не могу переименовать этот канал" -#: adminutils\adminutils.py:400 +#: adminutils\adminutils.py:438 msgid "I am unable to rename this channel: {}" msgstr "Невозможно переименовать канал: {}" -#: adminutils\adminutils.py:408 -#, docstring -msgid "Remove a channel from server\n\n" +#: adminutils\adminutils.py:449 +msgid "" +"Remove a channel from server\n" +"\n" " Example:\n" " `[p]channel delete channel`\n" " " -msgstr "Удалить канал с сервера\n\n" +msgstr "" +"Удалить канал с сервера\n" +"\n" " Пример:\n" " `[p]channel delete канал`\n" " " -#: adminutils\adminutils.py:418 -msgid "You are about to delete channel {channel}. This cannot be undone. Are you sure?\n" +#: adminutils\adminutils.py:459 +msgid "" +"You are about to delete channel {channel}. This cannot be undone. Are you " +"sure?\n" "To agree, type \"yes\"" -msgstr "Вы собираетесь удалить канал {channel}. Это действие не может быть отменено. Вы уверены в этом?\n" +msgstr "" +"Вы собираетесь удалить канал {channel}. Это действие не может быть отменено. " +"Вы уверены в этом?\n" "Для продолжения напишите \"yes\"" -#: adminutils\adminutils.py:432 +#: adminutils\adminutils.py:471 msgid "I can't delete this channel" msgstr "Я не могу удалить этот канал" -#: adminutils\adminutils.py:434 +#: adminutils\adminutils.py:473 msgid "I am unable to delete a channel: {}" msgstr "Невозможно удалить канал: {}" +#~ msgid "This image type is unsupported, or link is incorrect" +#~ msgstr "" +#~ "Данный тип изображений не поддерживается, или указана неверная ссылка" + +#~ msgid "" +#~ "This image type is not supported anymore or Discord returned incorrect " +#~ "data. Try again later." +#~ msgstr "" +#~ "Данный тип изображения больше не поддерживается, или Discord вернул " +#~ "неверную информацию. Попробуйте позже." diff --git a/datautils/__init__.py b/datautils/__init__.py index 6f2ada94..0b9c1033 100644 --- a/datautils/__init__.py +++ b/datautils/__init__.py @@ -17,8 +17,8 @@ async def setup_after_ready(bot): for alias in command.aliases: if bot.get_command(alias): command.aliases[command.aliases.index(alias)] = f"du{alias}" - bot.add_cog(cog) + await bot.add_cog(cog) -def setup(bot): +async def setup(bot): create_task(setup_after_ready(bot)) diff --git a/datautils/info.json b/datautils/info.json index 214cad7d..d9c13cc2 100644 --- a/datautils/info.json +++ b/datautils/info.json @@ -14,8 +14,7 @@ "server", "permissions" ], - "min_bot_version": "3.4.8", - "max_bot_version": "3.4.99", + "min_bot_version": "3.5.0", "requirements": [ "tabulate", "wcwidth", diff --git a/generalchannel/__init__.py b/generalchannel/__init__.py index 87299813..ef07eacb 100644 --- a/generalchannel/__init__.py +++ b/generalchannel/__init__.py @@ -5,5 +5,5 @@ ) -def setup(bot): - bot.add_cog(GeneralChannel(bot)) +async def setup(bot): + await bot.add_cog(GeneralChannel(bot)) diff --git a/generalchannel/info.json b/generalchannel/info.json index 59bf0be4..0e986459 100644 --- a/generalchannel/info.json +++ b/generalchannel/info.json @@ -5,7 +5,7 @@ "install_msg": "Thanks for install.", "short": "Allow users to manage #general channel's name and topic", "description": "Allow users to manage #general channel's name and topic", - "max_bot_version": "3.4.99", + "min_bot_version": "3.5.0", "tags": [ "#general", "channel", diff --git a/godvilledata/__init__.py b/godvilledata/__init__.py index ffd259b1..b988d297 100644 --- a/godvilledata/__init__.py +++ b/godvilledata/__init__.py @@ -6,5 +6,5 @@ ) -def setup(bot): - bot.add_cog(GodvilleData(bot)) +async def setup(bot): + await bot.add_cog(GodvilleData(bot)) diff --git a/godvilledata/godvilledata.py b/godvilledata/godvilledata.py index c3fb3c9d..369a8d21 100644 --- a/godvilledata/godvilledata.py +++ b/godvilledata/godvilledata.py @@ -5,11 +5,6 @@ from .godvilleuser import GodvilleUser -try: - from redbot import json # support of Draper's branch -except ImportError: - import json - BASE_API = "https://godville.net/gods/api" BASE_API_GLOBAL = "http://godvillegame.com/gods/api" @@ -49,7 +44,7 @@ async def convert(self, ctx, argument): class GodvilleData(commands.Cog): """Get data about Godville profiles""" - __version__ = "2.1.6" + __version__ = "3.0.0" # noinspection PyMissingConstructor def __init__(self, bot): @@ -60,7 +55,7 @@ def __init__(self, bot): "godvillegame": {"apikey": None, "godname": None}, } self.config.register_user(**default_user) - self.session = aiohttp.ClientSession(json_serialize=json.dumps) + self.session = aiohttp.ClientSession() def cog_unload(self): self.bot.loop.create_task(self.session.close()) @@ -90,7 +85,7 @@ async def godville(self, ctx, *, god: GodConverter): chat.error("Something went wrong. Server returned {}.".format(sg.status)) ) return - profile = await sg.json(loads=json.loads) + profile = await sg.json() profile = GodvilleUser(profile) text_header = "{} и его {}\n{}\n".format( chat.bold(profile.god), @@ -209,7 +204,7 @@ async def godvillegame(self, ctx, *, godname: str): chat.error("Something went wrong. Server returned {}.".format(sg.status)) ) return - profile = await sg.json(loads=json.loads) + profile = await sg.json() profile = GodvilleUser(profile) text_header = "{} and his {}\n{}\n".format( chat.bold(profile.god), diff --git a/godvilledata/info.json b/godvilledata/info.json index 718200ab..8e302eed 100644 --- a/godvilledata/info.json +++ b/godvilledata/info.json @@ -5,7 +5,7 @@ "install_msg": "Thanks for install.\n`[p]godville` shows info about russian version of game (godville.net)\n`[p]godvillegame` shows info about \"global\" version of game (godvillegame.com)", "short": "Get data about Godville profiles", "description": "Get data about godville.net (russian) and godvillegame.com profiles", - "max_bot_version": "3.4.99", + "min_bot_version": "3.5.0", "tags": [ "godville", "ZPG" diff --git a/godvilledata/locales/messages.pot b/godvilledata/locales/messages.pot index 31865768..e74d7ac7 100644 --- a/godvilledata/locales/messages.pot +++ b/godvilledata/locales/messages.pot @@ -2,14 +2,14 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2020-10-28 11:33+0400\n" +"POT-Creation-Date: 2023-06-01 17:50+0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: redgettext 3.3\n" +"Generated-By: redgettext 3.4.2\n" #: godvilledata\godvilledata.py:74 #, docstring diff --git a/leveler/__init__.py b/leveler/__init__.py index 2a7e4510..4ecb8ed8 100644 --- a/leveler/__init__.py +++ b/leveler/__init__.py @@ -22,5 +22,5 @@ async def setup(bot): cog = Leveler(bot) - bot.add_cog(cog) + await bot.add_cog(cog) await cog.initialize() diff --git a/leveler/abc.py b/leveler/abc.py index 8e37cbe1..a7fb21ad 100644 --- a/leveler/abc.py +++ b/leveler/abc.py @@ -2,7 +2,7 @@ from asyncio import Lock from io import BytesIO from logging import Logger -from typing import List +from typing import List, Literal, Optional, Union from aiohttp import ClientSession from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase @@ -124,7 +124,21 @@ async def _give_chat_credit(self, user, server): raise NotImplementedError @abstractmethod - async def _valid_image_url(self, url): + async def _valid_image_url( + self, url: str, *, guild_id: Optional[Union[Literal["global"], int]] = None + ): + raise NotImplementedError + + @abstractmethod + async def _download_image( + self, url: str, *, guild_id: Optional[Union[Literal["global"], int]] = None + ): + raise NotImplementedError + + @abstractmethod + async def _check_image_exists( + self, image_name: str, *, guild_id: Optional[Union[Literal["global"], int]] = None + ): raise NotImplementedError @abstractmethod diff --git a/leveler/commands/lvladmin/backgrounds.py b/leveler/commands/lvladmin/backgrounds.py index efe175ae..c25f56f0 100644 --- a/leveler/commands/lvladmin/backgrounds.py +++ b/leveler/commands/lvladmin/backgrounds.py @@ -1,5 +1,6 @@ from asyncio import TimeoutError as AsyncTimeoutError +import discord from redbot.core import commands from redbot.core.utils.predicates import MessagePredicate @@ -25,7 +26,7 @@ async def addprofilebg(self, ctx, name: str, url: str): """Add a profile background. The proportions must be 290px x 290px.""" - if not await self._valid_image_url(url): + if not await self._valid_image_url(url, guild_id=ctx.guild.id): await ctx.send("That is not a valid image URL!") return async with self.config.backgrounds() as backgrounds: @@ -54,7 +55,7 @@ async def addrankbg(self, ctx, name: str, url: str): """Add a rank background. The proportions must be 360px x 100px.""" - if not await self._valid_image_url(url): + if not await self._valid_image_url(url, guild_id=ctx.guild.id): await ctx.send("That is not a valid image URL!") return async with self.config.backgrounds() as backgrounds: @@ -83,7 +84,7 @@ async def addlevelbg(self, ctx, name: str, url: str): """Add a level-up background. The proportions must be 175px x 65px.""" - if not await self._valid_image_url(url): + if not await self._valid_image_url(url, guild_id=ctx.guild.id): await ctx.send("That is not a valid image URL!") return async with self.config.backgrounds() as backgrounds: @@ -108,7 +109,7 @@ async def addlevelbg(self, ctx, name: str, url: str): await ctx.send("New level-up background (`{}`) added.".format(name)) @lvladminbg.command() - async def setcustombg(self, ctx, bg_type: str, user_id: str, img_url: str): + async def setcustombg(self, ctx, bg_type: str, user: discord.User, img_url: str): """Set one-time custom background bg_type can be: `profile`, `rank` or `levelup`.""" @@ -120,20 +121,20 @@ async def setcustombg(self, ctx, bg_type: str, user_id: str, img_url: str): return # test if valid user_id - userinfo = await self.db.users.find_one({"user_id": str(user_id)}) + userinfo = await self.db.users.find_one({"user_id": str(user.id)}) if not userinfo: await ctx.send("That is not a valid user id!") return - if not await self._valid_image_url(img_url): + if not await self._valid_image_url(img_url, guild_id=user.id): await ctx.send("That is not a valid image URL!") return await self.db.users.update_one( - {"user_id": str(user_id)}, + {"user_id": str(user.id)}, {"$set": {"{}_background".format(type_input): img_url}}, ) - await ctx.send("User {} custom {} background set.".format(user_id, bg_type)) + await ctx.send("User {} custom {} background set.".format(user.id, bg_type)) @lvladminbg.command() async def delprofilebg(self, ctx, name: str): diff --git a/leveler/commands/lvladmin/badge.py b/leveler/commands/lvladmin/badge.py index dcbc8f08..1c5638e8 100644 --- a/leveler/commands/lvladmin/badge.py +++ b/leveler/commands/lvladmin/badge.py @@ -72,7 +72,7 @@ async def addbadge( await ctx.send("Name cannot contain `.`") return - if not await self._valid_image_url(bg_img): + if not await self._valid_image_url(bg_img, guild_id=serverid): await ctx.send("Background is not valid. Enter HEX color or image URL!") return @@ -357,7 +357,7 @@ async def listbadge(self, ctx): em = discord.Embed(description=page, colour=await ctx.embed_color()) em.set_author( name="Current Badge - Level Links for {}".format(server.name), - icon_url=server.icon_url, + icon_url=server.icon, ) em.set_footer(text=f"Page {i}/{len(pages)}") embeds.append(em) diff --git a/leveler/commands/lvladmin/roles.py b/leveler/commands/lvladmin/roles.py index 7c2ab85e..4e25508f 100644 --- a/leveler/commands/lvladmin/roles.py +++ b/leveler/commands/lvladmin/roles.py @@ -126,7 +126,7 @@ async def listrole(self, ctx): em = discord.Embed(description=page, colour=await ctx.embed_color()) em.set_author( name="Current Role - Level Links for {}".format(server.name), - icon_url=server.icon_url, + icon_url=server.icon, ) em.set_footer(text=f"Page {i}/{len(pages)}") embeds.append(em) diff --git a/leveler/commands/lvladmin/settings.py b/leveler/commands/lvladmin/settings.py index 49866438..36f38895 100644 --- a/leveler/commands/lvladmin/settings.py +++ b/leveler/commands/lvladmin/settings.py @@ -30,6 +30,9 @@ async def overview(self, ctx): "Level messages are private": bool_emojify( await self.config.guild(ctx.guild).private_lvl_message() ), + "Automatic Role Fixes enabled": bool_emojify( + await self.config.guild(ctx.guild).fix_roles() + ), } owner_settings = {} if is_owner: @@ -56,7 +59,7 @@ async def overview(self, ctx): chat.box(tabulate_settings(owner_settings.items())) if owner_settings else "" ) em.set_author( - name="Settings Overview for {}".format(ctx.guild.name), icon_url=ctx.guild.icon_url + name="Settings Overview for {}".format(ctx.guild.name), icon_url=ctx.guild.icon ) await ctx.send(embed=em) @@ -240,3 +243,24 @@ async def lvlmsglock(self, ctx): else: await self.config.guild(server).lvl_msg_lock.set(channel.id) await ctx.send("Level-up messages locked to `#{}`".format(channel.name)) + + @lvladmin.command(name="fixroles") + @commands.guild_only() + async def fixroles(self, ctx): + """Automatically fix roles for users on every exp gain. + + This will have the bot check every exp gain what roles the user + has and fix them according to the assigned roles. + + Note: This can be relatively expensive to do.""" + server = ctx.guild + + if await self.config.guild(server).fix_roles(): + await self.config.guild(server).lvl_msg_lock.clear() + await ctx.send( + "Automatic role fixing is disabled. " + "Users will only have their roles changed on levelup with the required level." + ) + else: + await self.config.guild(server).lvl_msg_lock.set(True) + await ctx.send("Automatic role fixing is enabled.") diff --git a/leveler/commands/lvladmin/users.py b/leveler/commands/lvladmin/users.py index c58ce0e5..92e5f5d4 100644 --- a/leveler/commands/lvladmin/users.py +++ b/leveler/commands/lvladmin/users.py @@ -1,3 +1,4 @@ +import asyncio import time from typing import Union @@ -41,6 +42,49 @@ async def xpban( else: await ctx.tick() + @commands.admin_or_permissions(manage_guild=True) + @lvladmin.command() + @commands.guild_only() + async def resetranks(self, ctx: commands.Context): + """ + Reset everyone's xp and level to zero. + + Roles will be fixed when the user next would earn exp. + """ + main_msg = "Fixing members {current}/{total}" + msg = await ctx.send(main_msg.format(current=0, total=len(ctx.guild.members))) + current = 0 + async with ctx.typing(): + for member in ctx.guild.members: + if member.bot: + continue + userinfo = await self.db.users.find_one({"user_id": str(member.id)}) + if userinfo is None: + # no point fixing users who have not been created yet, they might never speak! + continue + total_exp = userinfo["total_exp"] - userinfo.get(str(ctx.guild.id), {}).get( + "current_exp", 0 + ) + await self.db.users.update_one( + {"user_id": str(member.id)}, + { + "$set": { + "servers.{}.level".format(ctx.guild.id): 0, + "servers.{}.current_exp".format(ctx.guild.id): 0, + "total_exp": total_exp, + } + }, + ) + current += 1 + if current % 100 == 0: + await msg.edit( + content=main_msg.format(current=0, total=len(ctx.guild.members)) + ) + await asyncio.sleep(5) + await ctx.send( + "Finished resetting everyone's experience and levels. Roles will be reset automatically the next time they would earn exp." + ) + @commands.is_owner() @lvladmin.command() @commands.guild_only() diff --git a/leveler/commands/lvlset/badge.py b/leveler/commands/lvlset/badge.py index 6bcac17b..f1448ad6 100644 --- a/leveler/commands/lvlset/badge.py +++ b/leveler/commands/lvlset/badge.py @@ -29,11 +29,11 @@ async def badges_available(self, ctx, global_badges: bool = False): server = ctx.guild if global_badges: servername = "Global" - icon_url = self.bot.user.avatar_url + icon_url = self.bot.user.display_avatar serverid = "global" else: servername = server.name - icon_url = server.icon_url + icon_url = server.icon serverid = server.id server_badges = await self.db.badges.find_one({"server_id": str(serverid)}) if server_badges and (server_badges := server_badges["badges"]): diff --git a/leveler/commands/profiles.py b/leveler/commands/profiles.py index dcb575c4..863a9016 100644 --- a/leveler/commands/profiles.py +++ b/leveler/commands/profiles.py @@ -64,8 +64,8 @@ async def profile_text(self, user, server, userinfo): name="Badges:", value=(", ".join(userinfo["badges"]).replace("_", " ") or None), ) - em.set_author(name="Profile for {}".format(user.name), url=user.avatar_url) - em.set_thumbnail(url=user.avatar_url) + em.set_author(name="Profile for {}".format(user.name), url=user.display_avatar) + em.set_thumbnail(url=user.display_avatar) return em @commands.cooldown(1, 10, commands.BucketType.user) @@ -110,8 +110,8 @@ async def rank_text(self, user, server, userinfo): em.add_field(name="Reps", value=userinfo["rep"]) em.add_field(name="Server Level", value=userinfo["servers"][str(server.id)]["level"]) em.add_field(name="Server Exp", value=await self._find_server_exp(user, server)) - em.set_author(name="Rank & Statistics for {}".format(user.name), url=user.avatar_url) - em.set_thumbnail(url=user.avatar_url) + em.set_author(name="Rank & Statistics for {}".format(user.name), url=user.display_avatar) + em.set_thumbnail(url=user.display_avatar) return em @commands.command() @@ -153,6 +153,6 @@ async def lvlinfo(self, ctx, *, user: discord.Member = None): em = discord.Embed(description=chat.box(tabulate(data.items())), colour=user.colour) em.set_author( name="Profile Information for {}".format(user.name), - icon_url=user.avatar_url, + icon_url=user.display_avatar, ) await ctx.send(embed=em) diff --git a/leveler/commands/top.py b/leveler/commands/top.py index 1d9fbb14..f00e118f 100644 --- a/leveler/commands/top.py +++ b/leveler/commands/top.py @@ -60,7 +60,7 @@ async def top( await sleep(0) board_type = "Rep" - icon_url = self.bot.user.avatar_url + icon_url = self.bot.user.display_avatar elif options.global_top and owner: is_level = True if await self.config.global_levels() else False title = "Global Exp Leaderboard for {}\n".format(self.bot.user.name) @@ -106,7 +106,7 @@ async def top( await sleep(0) board_type = "Points" - icon_url = self.bot.user.avatar_url + icon_url = self.bot.user.display_avatar elif options.rep: title = "Rep Leaderboard for {}\n".format(server.name) async for userinfo in ( @@ -135,7 +135,7 @@ async def top( await sleep(0) board_type = "Rep" - icon_url = server.icon_url + icon_url = server.icon else: is_level = True title = "Exp Leaderboard for {}\n".format(server.name) @@ -177,14 +177,8 @@ async def top( ) await sleep(0) board_type = "Points" - icon_url = server.icon_url + icon_url = server.icon pages = TopPager(users, board_type, is_level, user_stat, icon_url, title) menu = TopMenu(pages) await menu.start(ctx) - page = options.page - if page > pages.get_max_pages(): - page = pages.get_max_pages() - if page < 1: - page = 1 - await menu.show_page(page - 1) diff --git a/leveler/def_imgen_utils.py b/leveler/def_imgen_utils.py index 1d1642a9..de5a60ab 100644 --- a/leveler/def_imgen_utils.py +++ b/leveler/def_imgen_utils.py @@ -1,16 +1,22 @@ import operator +import os import random import traceback import warnings from io import BytesIO +from logging import getLogger from math import floor, log +from pathlib import Path +from typing import Literal, Optional, Union +from redbot.core.data_manager import cog_data_path from redbot.core.errors import CogLoadError from .abc import MixinMeta try: from PIL import Image, ImageDraw + from PIL import features as pil_features except Exception as e: raise CogLoadError( f"Can't load pillow: {e}\n" @@ -37,19 +43,67 @@ ) +logger = getLogger("red.fixator10-cogs.leveler") +SAVE_FORMAT = "webp" if pil_features.check("webp") else "png" + + class DefaultImageGeneratorsUtils(MixinMeta): """Utils for default image generators""" - async def _valid_image_url(self, url): + async def _check_image_exists( + self, image_name: str, *, guild_id: Optional[Union[Literal["global"], int]] = None + ) -> Optional[Path]: + ret = None + if guild_id is not None: + path = cog_data_path(self).joinpath(str(guild_id)) + if not path.is_dir(): + path.mkdir(exist_ok=True, parents=True) + path = path.joinpath(image_name) + if os.path.isfile(path) and os.path.getsize(path) != 0: + ret = path + + global_path = cog_data_path(self).joinpath("global") + if not global_path.is_dir(): + global_path.mkdir(exist_ok=True, parents=True) + global_path = global_path.joinpath(image_name) + + if os.path.isfile(global_path) and os.path.getsize(global_path) != 0: + ret = global_path + return ret + + async def _download_image( + self, url: str, *, guild_id: Optional[Union[Literal["global"], int]] = None + ) -> BytesIO: + filename = url.split("/")[-1] + path = await self._check_image_exists(filename, guild_id=guild_id) + if path is not None: + logger.debug("Image %s exists, returning saved version", filename) + with path.open("rb") as infile: + image = BytesIO(infile.read()) + return image + async with self.session.get(url) as r: + logger.debug("Image %s missing, downloading now", filename) + image = BytesIO(await r.content.read()) + try: + im = Image.open(image).convert("RGBA") + except IOError: + raise TypeError("The url provided is not a valid image") + guild_path = "global" if guild_id is None else str(guild_id) + path = cog_data_path(self).joinpath(guild_path) + if not path.is_dir(): + path.mkdir(exist_ok=True, parents=True) + path = path.joinpath(filename) + with path.open("wb") as outfile: + im.save(outfile, format=SAVE_FORMAT) + return image + + async def _valid_image_url( + self, url: str, *, guild_id: Optional[Union[Literal["global"], int]] = None + ): try: - async with self.session.get(url) as r: - image = await r.content.read() - image = BytesIO(image) - await self.asyncify((im := await self.asyncify(Image.open, image)).convert, "RGBA") - im.close() - image.close() + await self._download_image(url, guild_id=guild_id) return True - except IOError: + except TypeError: return False # uses k-means algorithm to find color from bg, rank is abundance of color, descending @@ -58,9 +112,10 @@ async def _auto_color(self, ctx, url: str, ranks): await ctx.send("{}".format(random.choice(phrases))) clusters = 10 - async with self.session.get(url) as r: - image = await r.content.read() - image = BytesIO(image) + try: + image = await self._download_image(url, guild_id=ctx.guild.id) + except TypeError: + raise im = Image.open(image).convert("RGBA") im = im.resize((290, 290)) # resized to reduce time diff --git a/leveler/exp.py b/leveler/exp.py index f2277461..b94dc045 100644 --- a/leveler/exp.py +++ b/leveler/exp.py @@ -109,6 +109,47 @@ async def _process_exp(self, message, userinfo, exp: int): }, ) self.bot.dispatch("leveler_process_exp", message, exp) + if await self.config.guild(server).fix_roles(): + await self._handle_role_fixes(user, userinfo, server) + # check fixes for roles after we've already potentially added the roles from levelup + # so the message still gets sent. + + async def _handle_role_fixes( + self, user: discord.Member, userinfo: dict, server: discord.Guild + ): + # This is meant to silently fix roles from leveler + new_level = str(userinfo["servers"][str(server.id)]["level"]) + # add to appropriate role if necessary + # try: + server_roles = await self.db.roles.find_one({"server_id": str(server.id)}) + removed_roles = set() + added_roles = set() + user_roles = {r for r in user.roles} + if server_roles is not None: + for role in server_roles["roles"].keys(): + if int(server_roles["roles"][role]["level"]) > int(new_level): + continue + add_role = discord.utils.get(server.roles, name=role) + if add_role is not None: + added_roles.add(add_role) + remove_role = discord.utils.get( + server.roles, name=server_roles["roles"][role]["remove_role"] + ) + if remove_role is not None: + removed_roles.add(remove_role) + + added = removed_roles.symmetric_difference(added_roles) - user_roles + removed = user_roles.intersection(removed_roles) + if added: + try: + await user.add_roles(*added, reason="Levelup") + except (discord.Forbidden, discord.HTTPException): + pass + if removed: + try: + await user.remove_roles(*removed, reason="Levelup") + except (discord.Forbidden, discord.HTTPException): + pass async def _handle_levelup(self, user, userinfo, server, channel): # channel lock implementation @@ -130,7 +171,7 @@ async def _handle_levelup(self, user, userinfo, server, channel): server_roles = await self.db.roles.find_one({"server_id": str(server.id)}) if server_roles is not None: for role in server_roles["roles"].keys(): - if int(server_roles["roles"][role]["level"]) == int(new_level): + if int(new_level) == int(server_roles["roles"][role]["level"]): add_role = discord.utils.get(server.roles, name=role) if add_role is not None: try: @@ -153,7 +194,7 @@ async def _handle_levelup(self, user, userinfo, server, channel): server_linked_badges = await self.db.badgelinks.find_one({"server_id": str(server.id)}) if server_linked_badges is not None: for badge_name in server_linked_badges["badges"]: - if int(server_linked_badges["badges"][badge_name]) == int(new_level): + if int(new_level) >= int(server_linked_badges["badges"][badge_name]): server_badges = await self.db.badges.find_one( {"server_id": str(server.id)} ) diff --git a/leveler/image_generators.py b/leveler/image_generators.py index b0631ccd..fa3a0ba7 100644 --- a/leveler/image_generators.py +++ b/leveler/image_generators.py @@ -840,13 +840,13 @@ async def draw_rank(self, user, server): userinfo = await self.db.users.find_one({"user_id": str(user.id)}) # get urls bg_url = userinfo["rank_background"] - - async with self.session.get(bg_url) as r: - image = await r.content.read() - rank_background = BytesIO(image) + try: + rank_background = await self._download_image(bg_url) + except TypeError: + raise rank_avatar = BytesIO() try: - await user.avatar_url_as(format=AVATAR_FORMAT).save(rank_avatar) + await user.display_avatar.replace(format=AVATAR_FORMAT).save(rank_avatar) except discord.HTTPException: rank_avatar = f"{bundled_data_path(self)}/defaultavatar.png" @@ -869,13 +869,13 @@ async def draw_levelup(self, user, server): # get urls bg_url = userinfo["levelup_background"] - - async with self.session.get(bg_url) as r: - image = await r.content.read() - level_background = BytesIO(image) + try: + level_background = await self._download_image(bg_url) + except TypeError: + raise level_avatar = BytesIO() try: - await user.avatar_url_as(format=AVATAR_FORMAT).save(level_avatar) + await user.display_avatar.replace(format=AVATAR_FORMAT).save(level_avatar) except discord.HTTPException: level_avatar = f"{bundled_data_path(self)}/defaultavatar.png" @@ -890,12 +890,10 @@ async def draw_profile(self, user, server): userinfo = await self._badge_convert_dict(userinfo) bg_url = userinfo["profile_background"] - async with self.session.get(bg_url) as r: - image = await r.content.read() - profile_background = BytesIO(image) + profile_background = await self._download_image(bg_url) profile_avatar = BytesIO() try: - await user.avatar_url_as(format=AVATAR_FORMAT).save(profile_avatar) + await user.display_avatar.replace(format=AVATAR_FORMAT).save(profile_avatar) except discord.HTTPException: profile_avatar.close() profile_avatar = f"{bundled_data_path(self)}/defaultavatar.png" @@ -915,11 +913,10 @@ async def draw_profile(self, user, server): badges_images = [] async for badge in AsyncIter(sorted_badges[:9]): bg_color = badge[0]["bg_img"] - if await self._valid_image_url(bg_color): - async with self.session.get(bg_color) as r: - image = await r.content.read() - badges_images.append(BytesIO(image)) - else: + try: + bg_image = await self._download_image(bg_color, guild_id=server.id) + badges_images.append(bg_image) + except TypeError: badges_images.append(None) file = await self.asyncify_thread( diff --git a/leveler/info.json b/leveler/info.json index 48c3913d..28505279 100644 --- a/leveler/info.json +++ b/leveler/info.json @@ -13,8 +13,8 @@ "v2 cog", "mongodb" ], - "min_bot_version": "3.4.6", - "max_bot_version": "3.4.99", + "min_bot_version": "3.5.0", + "max_bot_version": "3.5.99", "requirements": [ "pymongo>=3.10,<3.12.3", "motor>=2.5,<3.0", diff --git a/leveler/leveler.py b/leveler/leveler.py index 61977f93..0e4c24df 100644 --- a/leveler/leveler.py +++ b/leveler/leveler.py @@ -84,6 +84,7 @@ def __init__(self, bot: Red): "lvl_msg_lock": None, "msg_credits": 0, "ignored_channels": [], + "fix_roles": False, } self.config.init_custom("MONGODB", -1) self.config.register_custom("MONGODB", **default_mongodb) diff --git a/leveler/menus/backgrounds.py b/leveler/menus/backgrounds.py index 66a87205..087cf07c 100644 --- a/leveler/menus/backgrounds.py +++ b/leveler/menus/backgrounds.py @@ -1,8 +1,32 @@ +from typing import Dict + import discord from redbot.vendored.discord.ext import menus +from .base import BaseView + + +class ChangeSourceButton(discord.ui.Button): + def __init__(self, label: str, source: menus.PageSource): + super().__init__(style=discord.ButtonStyle.grey, label=label) + self.source = source + + async def callback(self, interaction: discord.Interaction): + kwargs = await self.view.change_source(self.source) + await interaction.response.edit_message(**kwargs) + + +class BackgroundMenu(BaseView): + def __init__(self, sources: Dict[str, menus.PageSource], style: str): + self.sources = sources + self.bg_type = style + super().__init__(source=self.sources[style]) + for key, value in self.sources.items(): + nb = ChangeSourceButton(key, value) + self.add_item(nb) + -class BackgroundMenu(menus.MenuPages, inherit_buttons=False): +class BackgroundMenu_old(menus.MenuPages, inherit_buttons=False): def __init__( self, sources: dict, @@ -91,6 +115,10 @@ async def stop_pages(self, payload: discord.RawReactionActionEvent) -> None: class BackgroundPager(menus.ListPageSource): def __init__(self, entries): super().__init__(entries, per_page=1) + self.select_options = [ + discord.SelectOption(label=x[0], description=f"Page {num+1}", value=str(num)) + for num, x in enumerate(entries) + ] async def format_page(self, menu: BackgroundMenu, page): name, url = page diff --git a/leveler/menus/badges.py b/leveler/menus/badges.py index 201683ec..a60f6dd2 100644 --- a/leveler/menus/badges.py +++ b/leveler/menus/badges.py @@ -1,9 +1,43 @@ import discord +from redbot.core import commands from redbot.core.bank import get_currency_name from redbot.vendored.discord.ext import menus +from .base import BaseView -class BadgeMenu(menus.MenuPages, inherit_buttons=False): + +class BuyBadgeButton(discord.ui.Button): + def __init__( + self, + label: str, + emoji: str, + ): + super().__init__(style=discord.ButtonStyle.grey, label=label, emoji=emoji) + + async def callback(self, interaction: discord.Interaction): + await interaction.response.defer() + page = await self.view.source.get_page(self.view.current_page) + await self.view.ctx.invoke( + self.view.ctx.cog.buy_badge, + is_global=True if page["server_id"] == "global" else False, + name=page["badge_name"], + ) + + +class BadgeMenu(BaseView): + def __init__(self, source: menus.PageSource, timeout: int = 180, can_buy: bool = False): + super().__init__(source, timeout=timeout) + self.can_buy = can_buy + self.buy_button = BuyBadgeButton(label="Buy", emoji="\N{BANKNOTE WITH DOLLAR SIGN}") + self.add_item(self.buy_button) + + async def start(self, ctx: commands.Context): + if self.can_buy: + self.can_buy = await ctx.cog.buy_badge.can_run(ctx, check_all_parents=True) + await super().start(ctx) + + +class BadgeMenu_old(menus.MenuPages, inherit_buttons=False): def __init__( self, source: menus.PageSource, @@ -107,6 +141,14 @@ def __init__(self, entries, server_name, server_id, icon): self.server_name = server_name self.icon = icon self.server_id = server_id + self.select_options = [ + discord.SelectOption( + label=x.get("badge_name", "No Name Badge"), + description=f"Page {num+1}", + value=str(num), + ) + for num, x in enumerate(entries) + ] async def format_page(self, menu: BadgeMenu, page): em = discord.Embed( @@ -130,6 +172,14 @@ class OwnBadgePager(menus.ListPageSource): def __init__(self, entries, user: discord.Member): super().__init__(entries, per_page=1) self.user = user + self.select_options = [ + discord.SelectOption( + label=x.get("badge_name", "No Name Badge"), + description=f"Page {num+1}", + value=str(num), + ) + for num, x in enumerate(entries) + ] async def format_page(self, menu: BadgeMenu, page): em = discord.Embed( @@ -137,7 +187,7 @@ async def format_page(self, menu: BadgeMenu, page): description=page["description"], color=int(page["border_color"][1:], base=16), ) - em.set_author(name=self.user.display_name, icon_url=self.user.avatar_url) + em.set_author(name=self.user.display_name, icon_url=self.user.display_avatar) em.set_thumbnail(url=page["bg_img"]) em.set_footer( text=f"Server: {page['server_name']} • Badge {menu.current_page+1}/{self.get_max_pages()}" diff --git a/leveler/menus/base.py b/leveler/menus/base.py new file mode 100644 index 00000000..369c6789 --- /dev/null +++ b/leveler/menus/base.py @@ -0,0 +1,248 @@ +from typing import Any, List, Optional + +import discord +from redbot.core import commands +from redbot.vendored.discord.ext import menus + + +class StopButton(discord.ui.Button): + def __init__( + self, + style: discord.ButtonStyle, + row: Optional[int], + ): + super().__init__(style=style, row=row) + self.style = style + self.emoji = "\N{HEAVY MULTIPLICATION X}\N{VARIATION SELECTOR-16}" + + async def callback(self, interaction: discord.Interaction): + self.view.stop() + if interaction.message.flags.ephemeral: + await interaction.response.edit_message(view=None) + return + await interaction.message.delete() + + +class ForwardButton(discord.ui.Button): + def __init__( + self, + style: discord.ButtonStyle, + row: Optional[int], + ): + super().__init__(style=style, row=row) + self.style = style + self.emoji = "\N{BLACK RIGHT-POINTING TRIANGLE}\N{VARIATION SELECTOR-16}" + + async def callback(self, interaction: discord.Interaction): + await self.view.show_checked_page(self.view.current_page + 1, interaction) + + +class BackButton(discord.ui.Button): + def __init__( + self, + style: discord.ButtonStyle, + row: Optional[int], + ): + super().__init__(style=style, row=row) + self.style = style + self.emoji = "\N{BLACK LEFT-POINTING TRIANGLE}\N{VARIATION SELECTOR-16}" + + async def callback(self, interaction: discord.Interaction): + await self.view.show_checked_page(self.view.current_page - 1, interaction) + + +class LastItemButton(discord.ui.Button): + def __init__( + self, + style: discord.ButtonStyle, + row: Optional[int], + ): + super().__init__(style=style, row=row) + self.style = style + self.emoji = ( + "\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}" + ) + + async def callback(self, interaction: discord.Interaction): + await self.view.show_page(self.view._source.get_max_pages() - 1, interaction) + + +class FirstItemButton(discord.ui.Button): + def __init__( + self, + style: discord.ButtonStyle, + row: Optional[int], + ): + super().__init__(style=style, row=row) + self.style = style + self.emoji = ( + "\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}" + ) + + async def callback(self, interaction: discord.Interaction): + await self.view.show_page(0, interaction) + + +class _SelectMenu(discord.ui.Select): + def __init__(self, options: List[discord.SelectOption]): + super().__init__(placeholder="Select a Page", min_values=1, max_values=1, options=options) + + async def callback(self, interaction: discord.Interaction): + index = int(self.values[0]) + await self.view.show_page(index, interaction) + + +class BaseView(discord.ui.View): + def __init__( + self, + source: menus.PageSource, + clear_reactions_after: bool = True, + delete_message_after: bool = False, + timeout: int = 180, + message: discord.Message = None, + page_start: int = 0, + **kwargs: Any, + ) -> None: + super().__init__( + timeout=timeout, + ) + self._source = source + self.page_start = page_start + self.current_page = page_start + self.message = message + self.ctx = kwargs.get("ctx", None) + self.forward_button = ForwardButton(discord.ButtonStyle.grey, 0) + self.back_button = BackButton(discord.ButtonStyle.grey, 0) + self.first_item = FirstItemButton(discord.ButtonStyle.grey, 0) + self.last_item = LastItemButton(discord.ButtonStyle.grey, 0) + self.stop_button = StopButton(discord.ButtonStyle.red, 0) + self.add_item(self.stop_button) + self.add_item(self.first_item) + self.add_item(self.back_button) + self.add_item(self.forward_button) + self.add_item(self.last_item) + self.select_menu = self._get_select_menu() + self.add_item(self.select_menu) + + @property + def source(self): + return self._source + + async def on_timeout(self): + await self.message.edit(view=None) + + async def start(self, ctx: commands.Context): + await self.send_initial_message(ctx) + + def select_options(self): + return getattr(self.source, "select_options", []) + + async def change_source(self, new_source: menus.PageSource): + self.current_page = 0 + self._source = new_source + if not self.source.is_paginating(): + self.disable_navigation() + page = await self._source.get_page(self.page_start) + return await self._get_kwargs_from_page(page) + + def _get_select_menu(self): + # handles modifying the select menu if more than 25 pages are provided + # this will show the previous 12 and next 13 pages in the select menu + # based on the currently displayed page. Once you reach close to the max + # pages it will display the last 25 pages. + if len(self.select_options()) > 25: + minus_diff = None + plus_diff = 25 + if 12 < self.current_page < len(self.select_options()) - 25: + minus_diff = self.current_page - 12 + plus_diff = self.current_page + 13 + elif self.current_page >= len(self.select_options()) - 25: + minus_diff = len(self.select_options()) - 25 + plus_diff = None + options = self.select_options()[minus_diff:plus_diff] + else: + options = self.select_options()[:25] + return _SelectMenu(options) + + def disable_navigation(self): + self.first_item.disabled = True + self.back_button.disabled = True + self.forward_button.disabled = True + self.last_item.disabled = True + + def enable_navigation(self): + self.first_item.disabled = False + self.back_button.disabled = False + self.forward_button.disabled = False + self.last_item.disabled = False + + async def send_initial_message(self, ctx: commands.Context): + """|coro| + The default implementation of :meth:`Menu.send_initial_message` + for the interactive pagination session. + This implementation shows the first page of the source. + """ + self.ctx = ctx + if not self.source.is_paginating(): + self.disable_navigation() + page = await self._source.get_page(self.page_start) + kwargs = await self._get_kwargs_from_page(page) + self.message = await ctx.send(**kwargs, view=self) + return self.message + + async def _get_kwargs_from_page(self, page): + value = await discord.utils.maybe_coroutine(self._source.format_page, self, page) + if isinstance(value, dict): + return value + elif isinstance(value, str): + return {"content": value, "embed": None} + elif isinstance(value, discord.Embed): + return {"embed": value, "content": None} + + async def get_page(self, page_number: int): + if not self.source.is_paginating(): + self.disable_navigation() + else: + self.enable_navigation() + page = await self._source.get_page(page_number) + self.current_page = page_number + return await self._get_kwargs_from_page(page) + + async def show_page(self, page_number: int, interaction: discord.Interaction): + if not self.source.is_paginating(): + self.disable_navigation() + else: + self.enable_navigation() + if len(self.source.select_options) > 25 and self.source.is_paginating(): + self.remove_item(self.select_menu) + self.select_menu = self._get_select_menu() + self.add_item(self.select_menu) + page = await self._source.get_page(page_number) + self.current_page = page_number + kwargs = await self._get_kwargs_from_page(page) + await interaction.response.edit_message(**kwargs, view=self) + + async def show_checked_page(self, page_number: int, interaction: discord.Interaction) -> None: + max_pages = self._source.get_max_pages() + try: + if max_pages is None: + # If it doesn't give maximum pages, it cannot be checked + await self.show_page(page_number, interaction) + elif page_number >= max_pages: + await self.show_page(0, interaction) + elif page_number < 0: + await self.show_page(max_pages - 1, interaction) + elif max_pages > page_number >= 0: + await self.show_page(page_number, interaction) + except IndexError: + # An error happened that can be handled, so ignore it. + pass + + async def interaction_check(self, interaction: discord.Interaction): + """Just extends the default reaction_check to use owner_ids""" + if interaction.user.id not in (*self.ctx.bot.owner_ids, self.ctx.author.id): + await interaction.response.send_message( + content="You are not authorized to interact with this.", ephemeral=True + ) + return False + return True diff --git a/leveler/menus/top.py b/leveler/menus/top.py index cda07c6d..bb80115f 100644 --- a/leveler/menus/top.py +++ b/leveler/menus/top.py @@ -7,8 +7,15 @@ from redbot.vendored.discord.ext import menus from tabulate import tabulate +from .base import BaseView -class TopMenu(menus.MenuPages, inherit_buttons=False): + +class TopMenu(BaseView): + def __init__(self, source: menus.PageSource, timeout: int = 180): + super().__init__(source, timeout=timeout) + + +class TopMenu_old(menus.MenuPages, inherit_buttons=False): def __init__( self, source: menus.PageSource, @@ -111,6 +118,12 @@ def __init__( self.user = user_stats self.icon_url = icon_url self.title = title + pages, left_over = divmod(len(entries), self.per_page) + if left_over: + pages += 1 + self.select_options = [ + discord.SelectOption(label=f"Page {num+1}", value=str(num)) for num in range(0, pages) + ] async def format_page(self, menu: TopMenu, entries): table = tabulate( diff --git a/massthings/__init__.py b/massthings/__init__.py index 5c1f238b..67d793a0 100644 --- a/massthings/__init__.py +++ b/massthings/__init__.py @@ -6,5 +6,5 @@ ) -def setup(bot): - bot.add_cog(MassThings(bot)) +async def setup(bot): + await bot.add_cog(MassThings(bot)) diff --git a/massthings/info.json b/massthings/info.json index d46af45e..a66e6365 100644 --- a/massthings/info.json +++ b/massthings/info.json @@ -6,8 +6,7 @@ "short": "⚠Cog for doing things in bulk.⚠", "description": "⚠Cog for doing things in bulk.⚠\n⚠️ This cog may contain commands that may (or may not) be against Discord API terms. Use this at your own risk, cog author is not responsible for anything that happens during usage of this cog.", "tags": [], - "min_bot_version": "3.0.0", - "max_bot_version": "3.4.99", + "min_bot_version": "3.5.0", "requirements": ["tabulate"], "end_user_data_statement": "This cog does not persistently store data or metadata about users.", "hidden": true diff --git a/messageslog/__init__.py b/messageslog/__init__.py index 7bf21ec2..1bfd36d6 100644 --- a/messageslog/__init__.py +++ b/messageslog/__init__.py @@ -8,4 +8,4 @@ async def setup(bot): cog = MessagesLog(bot) await cog.initialize() - bot.add_cog(cog) + await bot.add_cog(cog) diff --git a/messageslog/info.json b/messageslog/info.json index ef81c761..8b882eb5 100644 --- a/messageslog/info.json +++ b/messageslog/info.json @@ -5,8 +5,7 @@ "install_msg": "Thanks for install.", "short": "Log deleted and edited messages to a specified channel", "description": "Log deleted and edited messages to a specified channel", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.99", + "min_bot_version": "3.5.0", "tags": [ "messages", "logs" diff --git a/messageslog/messageslog.py b/messageslog/messageslog.py index 1ffd96fe..5cea4daa 100644 --- a/messageslog/messageslog.py +++ b/messageslog/messageslog.py @@ -272,7 +272,7 @@ async def message_deleted(self, message: discord.Message): for a in message.attachments ), ) - embed.set_author(name=message.author, icon_url=message.author.avatar_url) + embed.set_author(name=message.author, icon_url=message.author.display_avatar) embed.set_footer(text=_("ID: {} • Sent at").format(message.id)) embed.add_field(name=_("Channel"), value=message.channel.mention) try: @@ -426,7 +426,7 @@ async def message_edited(self, before: discord.Message, after: discord.Message): for a in before.attachments ), ) - embed.set_author(name=before.author, icon_url=before.author.avatar_url) + embed.set_author(name=before.author, icon_url=before.author.display_avatar) embed.set_footer(text=_("ID: {} • Sent at").format(before.id)) try: await logchannel.send(embed=embed) diff --git a/minecraftdata/__init__.py b/minecraftdata/__init__.py index b6565095..26c1b762 100644 --- a/minecraftdata/__init__.py +++ b/minecraftdata/__init__.py @@ -5,5 +5,5 @@ ) -def setup(bot): - bot.add_cog(MinecraftData(bot)) +async def setup(bot): + await bot.add_cog(MinecraftData(bot)) diff --git a/minecraftdata/info.json b/minecraftdata/info.json index 2ee3ecb6..04cc6e2d 100644 --- a/minecraftdata/info.json +++ b/minecraftdata/info.json @@ -10,7 +10,7 @@ "game" ], "min_python_version": [3, 8, 0], - "max_bot_version": "3.4.99", + "min_bot_version": "3.5.0", "requirements": [ "tabulate", "wcwidth", diff --git a/moreutils/__init__.py b/moreutils/__init__.py index dc141c09..ff88fe8d 100644 --- a/moreutils/__init__.py +++ b/moreutils/__init__.py @@ -17,8 +17,8 @@ async def setup_after_ready(bot): for alias in command.aliases: if bot.get_command(alias): command.aliases[command.aliases.index(alias)] = f"mu{alias}" - bot.add_cog(cog) + await bot.add_cog(cog) -def setup(bot): +async def setup(bot): create_task(setup_after_ready(bot)) diff --git a/moreutils/info.json b/moreutils/info.json index e7a745bb..5296f139 100644 --- a/moreutils/info.json +++ b/moreutils/info.json @@ -12,7 +12,7 @@ "someone", "color" ], - "max_bot_version": "3.4.99", + "min_bot_version": "3.5.0", "requirements": [ "tabulate" ], diff --git a/personalroles/__init__.py b/personalroles/__init__.py index 622b5a2e..243a6764 100644 --- a/personalroles/__init__.py +++ b/personalroles/__init__.py @@ -1,3 +1,5 @@ +from redbot.core.bot import Red + from .personalroles import PersonalRoles __red_end_user_data_statement__ = ( @@ -6,5 +8,6 @@ ) -def setup(bot): - bot.add_cog(PersonalRoles(bot)) +async def setup(bot: Red): + cog = PersonalRoles(bot) + await bot.add_cog(cog) diff --git a/personalroles/info.json b/personalroles/info.json index c717fced..8b1ed4c1 100644 --- a/personalroles/info.json +++ b/personalroles/info.json @@ -5,8 +5,7 @@ "install_msg": "Thanks for install.", "short": "Personal roles for members", "description": "Personal roles for members", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.99", + "min_bot_version": "3.5.0", "tags": [ "myrole", "personal role", diff --git a/personalroles/personalroles.py b/personalroles/personalroles.py index 4e8ba791..a215051c 100644 --- a/personalroles/personalroles.py +++ b/personalroles/personalroles.py @@ -1,10 +1,11 @@ from asyncio import TimeoutError as AsyncTimeoutError from textwrap import shorten -from typing import Union +from typing import Dict, List, Literal, Optional, Union import aiohttp import discord from redbot.core import commands +from redbot.core.bot import Red from redbot.core.config import Config from redbot.core.i18n import Translator, cog_i18n, set_contextual_locales_from_guild from redbot.core.utils import AsyncIter @@ -14,28 +15,34 @@ from redbot.core.utils.predicates import ReactionPredicate from tabulate import tabulate -from .discord_py_future import edit_role_icon - try: from redbot import json # support of Draper's branch except ImportError: import json +RequestType = Literal["discord_deleted_user", "owner", "user", "user_strict"] + _ = Translator("PersonalRoles", __file__) -async def has_assigned_role(ctx): - """Check if user has assigned role""" - if not ctx.guild: - return False - return ctx.guild.get_role(await ctx.cog.config.member(ctx.author).role()) +def has_assigned_role(): + async def _predicate(ctx: commands.Context) -> bool: + if not ctx.guild: + return False + role_id = await ctx.cog.config.member(ctx.author).role() + role = ctx.guild.get_role(role_id) + return role is not None + + return commands.check(_predicate) -async def role_icons_feature(ctx): - """Check for ROLE_ICONS feature""" - if not ctx.guild: - return False - return "ROLE_ICONS" in ctx.guild.features +def role_icons_feature(): + async def _predicate(ctx: commands.Context) -> bool: + if not ctx.guild: + return False + return "ROLE_ICONS" in ctx.guild.features + + return commands.check(_predicate) @cog_i18n(_) @@ -45,12 +52,18 @@ class PersonalRoles(commands.Cog): __version__ = "2.2.5" # noinspection PyMissingConstructor - def __init__(self, bot): - self.bot = bot - self.session = aiohttp.ClientSession(json_serialize=json.dumps) - self.config = Config.get_conf(self, identifier=0x3D86BBD3E2B744AE8AA8B5D986EB4DD8) - default_member = {"role": None} - default_guild = {"blacklist": [], "role_persistence": True} + def __init__(self, bot: Red): + self.bot: Red = bot + self.session: aiohttp.ClientSession = aiohttp.ClientSession(json_serialize=json.dumps) + self.config: Config = Config.get_conf(self, identifier=0x3D86BBD3E2B744AE8AA8B5D986EB4DD8) + default_member: Dict[str, Optional[int]] = { + "role": None, + "limit": None, + } + default_guild: Dict[str, Union[List[int], bool]] = { + "blacklist": [], + "role_persistence": True, + } self.config.register_member(**default_member) self.config.register_guild(**default_guild) @@ -59,14 +72,14 @@ def __init__(self, bot): for cmd in self.icon.walk_commands(): cmd._buckets = self._icon_cd - def cog_unload(self): - self.bot.loop.create_task(self.session.close()) + async def cog_unload(self): + await self.session.close() def format_help_for_context(self, ctx: commands.Context) -> str: # Thanks Sinbad! pre_processed = super().format_help_for_context(ctx) return f"{pre_processed}\n\n**Version**: {self.__version__}" - async def red_delete_data_for_user(self, *, requester, user_id: int): + async def red_delete_data_for_user(self, *, requester: RequestType, user_id: int): # Thanks Sinbad data = await self.config.all_members() async for guild_id, members in AsyncIter(data.items()): @@ -75,13 +88,13 @@ async def red_delete_data_for_user(self, *, requester, user_id: int): @commands.group() @commands.guild_only() - async def myrole(self, ctx): + async def myrole(self, ctx: commands.Context): """Control of personal role""" pass @myrole.command() @commands.admin_or_permissions(manage_roles=True) - async def assign(self, ctx, user: discord.Member, *, role: discord.Role): + async def assign(self, ctx: commands.Context, user: discord.Member, *, role: discord.Role): """Assign personal role to someone""" await self.config.member(user).role.set(role.id) await ctx.send( @@ -92,12 +105,14 @@ async def assign(self, ctx, user: discord.Member, *, role: discord.Role): @myrole.command() @commands.admin_or_permissions(manage_roles=True) - async def unassign(self, ctx, *, user: Union[discord.Member, discord.User, int]): + async def unassign( + self, ctx: commands.Context, *, user: Union[discord.Member, discord.User, int] + ): """Unassign personal role from someone""" if isinstance(user, discord.Member): - await self.config.member(user).role.clear() + await self.config.member(user).clear() elif isinstance(user, int): - await self.config.member_from_ids(ctx.guild.id, user).role.clear() + await self.config.member_from_ids(ctx.guild.id, user).clear() if _user := self.bot.get_user(user): user = _user else: @@ -111,7 +126,7 @@ async def unassign(self, ctx, *, user: Union[discord.Member, discord.User, int]) @myrole.command(name="list") @commands.admin_or_permissions(manage_roles=True) - async def mr_list(self, ctx): + async def mr_list(self, ctx: commands.Context): """Assigned roles list""" members_data = await self.config.all_members(ctx.guild) assigned_roles = [] @@ -136,7 +151,7 @@ async def mr_list(self, ctx): @myrole.command(name="persistence") @commands.admin_or_permissions(manage_roles=True) - async def mr_persistence(self, ctx): + async def mr_persistence(self, ctx: commands.Context): """Toggle auto-adding role on rejoin.""" editing = self.config.guild(ctx.guild).role_persistence new_state = not await editing() @@ -149,9 +164,30 @@ async def mr_persistence(self, ctx): ) ) + @myrole.command(name="limit") + @commands.admin_or_permissions(manage_roles=True) + async def mr_limit( + self, + ctx: commands.Context, + user: discord.Member, + amount: commands.Range[int, 1, 30] = None, + ): + """Give users permissions on how many users they can share their personal role with. + + Run this command without the `amount` argument to clear the limit config. + """ + if amount is None: + await self.config.member(user).limit.clear() + await ctx.send(f"Cleared the limit config for {user.display_name}.") + return + await self.config.member(user).limit.set(int(amount)) + await ctx.send( + f"{user.display_name} can now share their personal role with {int(amount)} friends." + ) + @myrole.group(name="blocklist", aliases=["blacklist"]) @commands.admin_or_permissions(manage_roles=True) - async def blacklist(self, ctx): + async def blacklist(self, ctx: commands.Context): """Manage blocklisted names""" pass @@ -170,7 +206,7 @@ async def add(self, ctx, *, rolename: str): ) @blacklist.command() - async def remove(self, ctx, *, rolename: str): + async def remove(self, ctx: commands.Context, *, rolename: str): """Remove rolename from blocklist""" rolename = rolename.casefold() async with self.config.guild(ctx.guild).blacklist() as blacklist: @@ -184,7 +220,7 @@ async def remove(self, ctx, *, rolename: str): @blacklist.command(name="list") @commands.admin_or_permissions(manage_roles=True) - async def bl_list(self, ctx): + async def bl_list(self, ctx: commands.Context): """List of blocklisted role names""" blacklist = await self.config.guild(ctx.guild).blacklist() pages = [chat.box(page) for page in chat.pagify("\n".join(blacklist))] @@ -193,11 +229,13 @@ async def bl_list(self, ctx): else: await ctx.send(chat.info(_("There is no blocklisted roles"))) + @has_assigned_role() @commands.cooldown(1, 30, commands.BucketType.member) @myrole.command(aliases=["color"]) - @commands.check(has_assigned_role) @commands.bot_has_permissions(manage_roles=True) - async def colour(self, ctx, *, colour: discord.Colour = discord.Colour.default()): + async def colour( + self, ctx: commands.Context, *, colour: discord.Colour = discord.Colour.default() + ): """Change color of personal role""" role = await self.config.member(ctx.author).role() role = ctx.guild.get_role(role) @@ -223,11 +261,11 @@ async def colour(self, ctx, *, colour: discord.Colour = discord.Colour.default() ) ) - @commands.cooldown(1, 30, commands.BucketType.member) @myrole.command() - @commands.check(has_assigned_role) + @has_assigned_role() + @commands.cooldown(1, 30, commands.BucketType.member) @commands.bot_has_permissions(manage_roles=True) - async def name(self, ctx, *, name: str): + async def name(self, ctx: commands.Context, *, name: str): """Change name of personal role You cant use blocklisted names""" role = await self.config.member(ctx.author).role() @@ -253,11 +291,11 @@ async def name(self, ctx, *, name: str): ) ) + @has_assigned_role() + @role_icons_feature() @myrole.group(invoke_without_command=True) - @commands.check(has_assigned_role) - @commands.check(role_icons_feature) @commands.bot_has_permissions(manage_roles=True) - async def icon(self, ctx): + async def icon(self, ctx: commands.Context): """Change icon of personal role""" pass @@ -285,17 +323,13 @@ async def icon_emoji(self, ctx, *, emoji: Union[discord.Emoji, discord.PartialEm return try: if isinstance(emoji, (discord.Emoji, discord.PartialEmoji)): - await edit_role_icon( - self.bot, - role, - icon=await emoji.url_as(format="png").read(), + await role.edit( + display_icon=await emoji.read(), reason=get_audit_reason(ctx.author, _("Personal Role")), ) else: - await edit_role_icon( - self.bot, - role, - unicode_emoji=emoji, + await role.edit( + display_icon=emoji, reason=get_audit_reason(ctx.author, _("Personal Role")), ) except discord.Forbidden: @@ -303,7 +337,7 @@ async def icon_emoji(self, ctx, *, emoji: Union[discord.Emoji, discord.PartialEm await ctx.send( chat.error(_("Unable to edit role.\nRole must be lower than my top role")) ) - except discord.InvalidArgument: + except ValueError: await ctx.send(chat.error(_("This image type is unsupported, or link is incorrect"))) except discord.HTTPException as e: ctx.command.reset_cooldown(ctx) @@ -314,7 +348,7 @@ async def icon_emoji(self, ctx, *, emoji: Union[discord.Emoji, discord.PartialEm ) @icon.command(name="image", aliases=["url"]) - async def icon_image(self, ctx, *, url: str = None): + async def icon_image(self, ctx: commands.Context, *, url: str = None): """Change icon of personal role using image""" role = await self.config.member(ctx.author).role() role = ctx.guild.get_role(role) @@ -330,18 +364,15 @@ async def icon_image(self, ctx, *, url: str = None): await ctx.send(chat.error(_("Unable to get image: {}").format(e.message))) return try: - await edit_role_icon( - self.bot, - role, - icon=image, - reason=get_audit_reason(ctx.author, _("Personal Role")), + await role.edit( + display_icon=image, reason=get_audit_reason(ctx.author, _("Personal Role")) ) except discord.Forbidden: ctx.command.reset_cooldown(ctx) await ctx.send( chat.error(_("Unable to edit role.\nRole must be lower than my top role")) ) - except discord.InvalidArgument: + except ValueError: await ctx.send(chat.error(_("This image type is unsupported, or link is incorrect"))) except discord.HTTPException as e: ctx.command.reset_cooldown(ctx) @@ -352,16 +383,13 @@ async def icon_image(self, ctx, *, url: str = None): ) @icon.command(name="reset", aliases=["remove"]) - async def icon_reset(self, ctx): + async def icon_reset(self, ctx: commands.Context): """Remove icon of personal role""" role = await self.config.member(ctx.author).role() role = ctx.guild.get_role(role) try: - await edit_role_icon( - self.bot, - role, - reason=get_audit_reason(ctx.author, _("Personal Role")), - reset=True, + await role.edit( + display_icon=None, reason=get_audit_reason(ctx.author, _("Personal Role")) ) await ctx.send( _("Removed icon of {user}'s personal role").format(user=ctx.message.author.name) @@ -375,11 +403,97 @@ async def icon_reset(self, ctx): ctx.command.reset_cooldown(ctx) await ctx.send(chat.error(_("Unable to edit role: {}").format(e))) + @has_assigned_role() + @myrole.command(aliases=["friend"]) + @commands.cooldown(1, 30, commands.BucketType.member) + async def friends( + self, + ctx: commands.Context, + add_or_remove: Literal["add", "remove"], + user: Optional[discord.Member] = None, + ): + """ + Add or remove friends from your role. + + `` should be either `add` to add or `remove` to remove friends. + """ + if user is None: + await ctx.send("`User` is a required argument.") + return + + role_id = await self.config.member(ctx.author).role() + role = ctx.guild.get_role(role_id) + limit = await self.config.member(ctx.author).limit() + + if add_or_remove.lower() == "add" and limit is None: + await ctx.send("You're not allowed to add you personal role to your friends.") + ctx.command.reset_cooldown(ctx) + return + + if add_or_remove.lower() == "add" and len(role.members) >= limit: + await ctx.send( + "You're at maximum capacity, you cannot add any more users to your role." + ) + ctx.command.reset_cooldown(ctx) + return + + added_or_not = discord.utils.get(user.roles, id=role.id) + + async with self.config.member(ctx.author).friends() as friends: + if add_or_remove.lower() == "add": + if added_or_not is None: + await ctx.send(f"{user.display_name} already has your personal role.") + return + else: + try: + await user.add_roles( + role, reason=get_audit_reason(ctx.author, _("Personal Role")) + ) + except discord.Forbidden: + ctx.command.reset_cooldown(ctx) + await ctx.send( + chat.error( + _("Unable to edit role.\nRole must be lower than my top role") + ) + ) + except discord.HTTPException as e: + ctx.command.reset_cooldown(ctx) + await ctx.send(chat.error(_("Unable to edit role: {}").format(e))) + elif add_or_remove.lower() == "remove": + if added_or_not: + await ctx.send(f"{user.display_name} does not have your personal role.") + return + else: + try: + await user.remove_roles( + role, reason=get_audit_reason(ctx.author, _("Personal Role")) + ) + except discord.Forbidden: + ctx.command.reset_cooldown(ctx) + await ctx.send( + chat.error( + _("Unable to edit role.\nRole must be lower than my top role") + ) + ) + except discord.HTTPException as e: + ctx.command.reset_cooldown(ctx) + await ctx.send(chat.error(_("Unable to edit role: {}").format(e))) + else: + await ctx.send("Not a valid `add_or_remove` option.") + return + + await ctx.send( + f"Successfully {'added' if add_or_remove.lower() == 'add' else 'removed'} " + f"your role {'to' if add_or_remove.lower() == 'add' else 'from'} {user.display_name}." + ) + @commands.Cog.listener("on_member_join") - async def role_persistence(self, member): + async def role_persistence(self, member: discord.Member): """Automatically give already assigned roles on join""" if await self.bot.cog_disabled_in_guild(self, member.guild): return + if not await self.bot.allowed_by_whitelist_blacklist(member): + return if not await self.config.guild(member.guild).role_persistence(): return await set_contextual_locales_from_guild(self.bot, member.guild) diff --git a/reverseimagesearch/info.json b/reverseimagesearch/info.json index c85bc345..07c270b8 100644 --- a/reverseimagesearch/info.json +++ b/reverseimagesearch/info.json @@ -5,8 +5,7 @@ "install_msg": "Thanks for install.", "short": "This cog allows users to reverse search images", "description": "This cog allows users to reverse search images", - "min_bot_version": "3.2.1", - "max_bot_version": "3.4.99", + "min_bot_version": "3.5.0", "tags": [ "reverse", "image", diff --git a/smmdata/__init__.py b/smmdata/__init__.py index d83cb795..6e07f79e 100644 --- a/smmdata/__init__.py +++ b/smmdata/__init__.py @@ -5,5 +5,5 @@ ) -def setup(bot): - bot.add_cog(SMMData(bot)) +async def setup(bot): + await bot.add_cog(SMMData(bot)) diff --git a/smmdata/info.json b/smmdata/info.json index 2ad51a05..1140f3a0 100644 --- a/smmdata/info.json +++ b/smmdata/info.json @@ -4,7 +4,7 @@ ], "install_msg": "Thanks for install.", "short": "This cog shows SMM-related data", - "max_bot_version": "3.4.99", + "min_bot_version": "3.5.0", "description": "This cog shows Super Mario Maker-related data. You can check levels and level designers", "tags": [ "mario", diff --git a/steamcommunity/info.json b/steamcommunity/info.json index 85e395ed..5de713c0 100644 --- a/steamcommunity/info.json +++ b/steamcommunity/info.json @@ -5,8 +5,7 @@ "install_msg": "Thanks for install.\nRemember to set api key (`[p]sc apikey`)", "short": "Get steamcommunity data", "description": "Get steamcommunity data for steam users", - "min_bot_version": "3.2.1", - "max_bot_version": "3.4.99", + "min_bot_version": "3.5.0", "tags": [ "steam", "steamcommunity" diff --git a/translators/__init__.py b/translators/__init__.py index 6d3750b2..e50b3a58 100644 --- a/translators/__init__.py +++ b/translators/__init__.py @@ -5,5 +5,5 @@ ) -def setup(bot): - bot.add_cog(Translators(bot)) +async def setup(bot): + await bot.add_cog(Translators(bot)) diff --git a/translators/info.json b/translators/info.json index c282dff8..353cfa06 100644 --- a/translators/info.json +++ b/translators/info.json @@ -5,8 +5,7 @@ "install_msg": "Thanks for install.\nFor Yandex.Translate you will need to set apikey (`[p]ytapikey`)", "short": "Useful (and not) translators", "description": "Useful (and not) translators", - "min_bot_version": "3.4.1", - "max_bot_version": "3.4.99", + "min_bot_version": "3.5.0", "tags": [ "translation", "translate", diff --git a/vocadb/__init__.py b/vocadb/__init__.py index 9603d6f1..d226fce3 100644 --- a/vocadb/__init__.py +++ b/vocadb/__init__.py @@ -3,5 +3,5 @@ __red_end_user_data_statement__ = "This cog does not persistently store any PII data about users." -def setup(bot): - bot.add_cog(VocaDB(bot)) +async def setup(bot): + await bot.add_cog(VocaDB(bot)) diff --git a/vocadb/info.json b/vocadb/info.json index 1e535a13..1f2e124c 100644 --- a/vocadb/info.json +++ b/vocadb/info.json @@ -11,8 +11,7 @@ "required_cogs": {}, "requirements": [], "tags": ["vocadb", "vocaloid"], - "min_bot_version": "3.4.12", - "max_bot_version": "3.4.99", + "min_bot_version": "3.5.0", "hidden": false, "disabled": false, "type": "COG" diff --git a/vocadb/vocadb.py b/vocadb/vocadb.py index 92646b0e..d4f8b217 100644 --- a/vocadb/vocadb.py +++ b/vocadb/vocadb.py @@ -134,7 +134,7 @@ def _lyrics_embed(colour, page: Dict[str, Any], data: Dict[str, Any]) -> discord title = [ x.get("value") for x in data.get("names") - if x.get("language") == LANGUAGE_MAP.get(page["cultureCode"]) + if x.get("language") == LANGUAGE_MAP.get(page.get("translationType")) ] em = discord.Embed( title=title[0] if title else data.get("defaultName"), @@ -156,7 +156,7 @@ def _lyrics_embed(colour, page: Dict[str, Any], data: Dict[str, Any]) -> discord @commands.cooldown(1, 5, commands.BucketType.user) async def vocadb(self, ctx: commands.Context, *, query: str): """Fetch Vocaloid song lyrics from VocaDB.net database""" - await ctx.trigger_typing() + await ctx.typing() data = await self._fetch_data(ctx, query) if type(data) == str: @@ -170,7 +170,7 @@ async def vocadb(self, ctx: commands.Context, *, query: str): embeds = [] for i, page in enumerate(data["lyrics"], start=1): - language = f"Language: {LANGUAGE_MAP.get(page.get('cultureCode', 'na'))}" + language = f"Version: {page.get('translationType', 'na')}" emb = self._lyrics_embed(await ctx.embed_colour(), page, data) emb.set_footer(text=f"{language} • Page {i} of {len(data['lyrics'])}") embeds.append(emb) diff --git a/weather/__init__.py b/weather/__init__.py index a11ac6b0..29c90540 100644 --- a/weather/__init__.py +++ b/weather/__init__.py @@ -6,5 +6,5 @@ ) -def setup(bot): - bot.add_cog(Weather(bot)) +async def setup(bot): + await bot.add_cog(Weather(bot)) diff --git a/weather/info.json b/weather/info.json index 041fd7e5..fc90115b 100644 --- a/weather/info.json +++ b/weather/info.json @@ -6,8 +6,7 @@ "install_msg": "Thanks for install.\nDon't forget to set api key (`[p]forecastapi`)\nIf you don't have registered DarkSky API key, this cog will be useless to you.\nDarkSky API closing at end of ~~2021~~ 2022 and im aware of this.", "short": "Weather forecast", "description": "Weather forecast. Requires \"semi-private\" DarkSky API key. For more info: https://blog.darksky.net", - "min_bot_version": "3.2.1", - "max_bot_version": "3.4.99", + "min_bot_version": "3.5.0", "tags": [ "weather", "forecast"