mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-21 18:27:59 -05:00
Merge branch 'V3/develop' into cog_guide_core
# Conflicts: # redbot/core/core_commands.py
This commit is contained in:
@@ -35,6 +35,17 @@ You can add additional Red based arguments after the instance, such as :code:`--
|
|||||||
<Red Instance>
|
<Red Instance>
|
||||||
The name of your Red instance.
|
The name of your Red instance.
|
||||||
|
|
||||||
|
If you used :code:`pyenv virtualenv` to create your virtual environment, please make the following changes to the above generated command
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
<Location to your Python Interpreter>
|
||||||
|
Run the following instead to get your Python interpreter
|
||||||
|
pyenv which python
|
||||||
|
|
||||||
|
Replace the `redbot` part of `pm2 start redbot` with the output of the following (when ran inside your activated venv)
|
||||||
|
pyenv which redbot
|
||||||
|
|
||||||
------------------------------
|
------------------------------
|
||||||
Ensuring that PM2 stays online
|
Ensuring that PM2 stays online
|
||||||
------------------------------
|
------------------------------
|
||||||
|
|||||||
@@ -2,5 +2,7 @@ from .filter import Filter
|
|||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: Red):
|
async def setup(bot: Red) -> None:
|
||||||
bot.add_cog(Filter(bot))
|
cog = Filter(bot)
|
||||||
|
await cog.initialize()
|
||||||
|
bot.add_cog(cog)
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ class Filter(commands.Cog):
|
|||||||
self.config.register_guild(**default_guild_settings)
|
self.config.register_guild(**default_guild_settings)
|
||||||
self.config.register_member(**default_member_settings)
|
self.config.register_member(**default_member_settings)
|
||||||
self.config.register_channel(**default_channel_settings)
|
self.config.register_channel(**default_channel_settings)
|
||||||
self.register_task = self.bot.loop.create_task(self.register_filterban())
|
|
||||||
self.pattern_cache = {}
|
self.pattern_cache = {}
|
||||||
|
|
||||||
async def red_delete_data_for_user(
|
async def red_delete_data_for_user(
|
||||||
@@ -55,17 +54,27 @@ class Filter(commands.Cog):
|
|||||||
if user_id in guild_data:
|
if user_id in guild_data:
|
||||||
await self.config.member_from_ids(guild_id, user_id).clear()
|
await self.config.member_from_ids(guild_id, user_id).clear()
|
||||||
|
|
||||||
def cog_unload(self):
|
async def initialize(self) -> None:
|
||||||
self.register_task.cancel()
|
await self.register_casetypes()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def register_filterban():
|
async def register_casetypes() -> None:
|
||||||
try:
|
await modlog.register_casetypes(
|
||||||
await modlog.register_casetype(
|
[
|
||||||
"filterban", False, ":filing_cabinet: :hammer:", "Filter ban"
|
{
|
||||||
)
|
"name": "filterban",
|
||||||
except RuntimeError:
|
"default_setting": False,
|
||||||
pass
|
"image": "\N{FILE CABINET}\N{VARIATION SELECTOR-16} \N{HAMMER}",
|
||||||
|
"case_str": "Filter ban",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "filterhit",
|
||||||
|
"default_setting": False,
|
||||||
|
"image": "\N{FILE CABINET}\N{VARIATION SELECTOR-16}",
|
||||||
|
"case_str": "Filter hit",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -183,7 +192,7 @@ class Filter(commands.Cog):
|
|||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await ctx.send(_("I can't send direct messages to you."))
|
await ctx.send(_("I can't send direct messages to you."))
|
||||||
|
|
||||||
@_filter_channel.command("add")
|
@_filter_channel.command(name="add", require_var_positional=True)
|
||||||
async def filter_channel_add(self, ctx: commands.Context, *words: str):
|
async def filter_channel_add(self, ctx: commands.Context, *words: str):
|
||||||
"""Add words to the filter.
|
"""Add words to the filter.
|
||||||
|
|
||||||
@@ -205,7 +214,7 @@ class Filter(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
await ctx.send(_("Words already in the filter."))
|
await ctx.send(_("Words already in the filter."))
|
||||||
|
|
||||||
@_filter_channel.command("remove")
|
@_filter_channel.command(name="delete", aliases=["remove", "del"], require_var_positional=True)
|
||||||
async def filter_channel_remove(self, ctx: commands.Context, *words: str):
|
async def filter_channel_remove(self, ctx: commands.Context, *words: str):
|
||||||
"""Remove words from the filter.
|
"""Remove words from the filter.
|
||||||
|
|
||||||
@@ -227,7 +236,7 @@ class Filter(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
await ctx.send(_("Those words weren't in the filter."))
|
await ctx.send(_("Those words weren't in the filter."))
|
||||||
|
|
||||||
@_filter.command(name="add")
|
@_filter.command(name="add", require_var_positional=True)
|
||||||
async def filter_add(self, ctx: commands.Context, *words: str):
|
async def filter_add(self, ctx: commands.Context, *words: str):
|
||||||
"""Add words to the filter.
|
"""Add words to the filter.
|
||||||
|
|
||||||
@@ -249,7 +258,7 @@ class Filter(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
await ctx.send(_("Those words were already in the filter."))
|
await ctx.send(_("Those words were already in the filter."))
|
||||||
|
|
||||||
@_filter.command(name="delete", aliases=["remove", "del"])
|
@_filter.command(name="delete", aliases=["remove", "del"], require_var_positional=True)
|
||||||
async def filter_remove(self, ctx: commands.Context, *words: str):
|
async def filter_remove(self, ctx: commands.Context, *words: str):
|
||||||
"""Remove words from the filter.
|
"""Remove words from the filter.
|
||||||
|
|
||||||
@@ -391,6 +400,20 @@ class Filter(commands.Cog):
|
|||||||
hits = await self.filter_hits(message.content, message.channel)
|
hits = await self.filter_hits(message.content, message.channel)
|
||||||
|
|
||||||
if hits:
|
if hits:
|
||||||
|
await modlog.create_case(
|
||||||
|
bot=self.bot,
|
||||||
|
guild=guild,
|
||||||
|
created_at=message.created_at.replace(tzinfo=timezone.utc),
|
||||||
|
action_type="filterhit",
|
||||||
|
user=author,
|
||||||
|
moderator=guild.me,
|
||||||
|
reason=(
|
||||||
|
_("Filtered words used: {words}").format(words=humanize_list(list(hits)))
|
||||||
|
if len(hits) > 1
|
||||||
|
else _("Filtered word used: {word}").format(word=list(hits)[0])
|
||||||
|
),
|
||||||
|
channel=message.channel,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
await message.delete()
|
await message.delete()
|
||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
@@ -471,7 +494,6 @@ class Filter(commands.Cog):
|
|||||||
await set_contextual_locales_from_guild(self.bot, guild)
|
await set_contextual_locales_from_guild(self.bot, guild)
|
||||||
|
|
||||||
if await self.filter_hits(member.display_name, member.guild):
|
if await self.filter_hits(member.display_name, member.guild):
|
||||||
|
|
||||||
name_to_use = guild_data["filter_default_name"]
|
name_to_use = guild_data["filter_default_name"]
|
||||||
reason = _("Filtered nickname") if member.nick else _("Filtered name")
|
reason = _("Filtered nickname") if member.nick else _("Filtered name")
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ class General(commands.Cog):
|
|||||||
""" Nothing to delete """
|
""" Nothing to delete """
|
||||||
return
|
return
|
||||||
|
|
||||||
@commands.command()
|
@commands.command(usage="<choice> <choices...>")
|
||||||
async def choose(self, ctx, *choices):
|
async def choose(self, ctx, *choices):
|
||||||
"""Choose between multiple options.
|
"""Choose between multiple options.
|
||||||
|
|
||||||
|
|||||||
@@ -270,7 +270,14 @@ class KickBanMixin(MixinMeta):
|
|||||||
@commands.bot_has_permissions(kick_members=True)
|
@commands.bot_has_permissions(kick_members=True)
|
||||||
@checks.admin_or_permissions(kick_members=True)
|
@checks.admin_or_permissions(kick_members=True)
|
||||||
async def kick(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
|
async def kick(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
|
||||||
"""Kick a user.
|
"""
|
||||||
|
Kick a user.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `[p]kick 428675506947227648 wanted to be kicked.`
|
||||||
|
This will kick Twentysix from the server.
|
||||||
|
- `[p]kick @Twentysix wanted to be kicked.`
|
||||||
|
This will kick Twentysix from the server.
|
||||||
|
|
||||||
If a reason is specified, it will be the reason that shows up
|
If a reason is specified, it will be the reason that shows up
|
||||||
in the audit log.
|
in the audit log.
|
||||||
@@ -349,11 +356,18 @@ class KickBanMixin(MixinMeta):
|
|||||||
):
|
):
|
||||||
"""Ban a user from this server and optionally delete days of messages.
|
"""Ban a user from this server and optionally delete days of messages.
|
||||||
|
|
||||||
|
`days` is the amount of days of messages to cleanup on ban.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `[p]ban 428675506947227648 7 Continued to spam after told to stop.`
|
||||||
|
This will ban Twentysix and it will delete 7 days worth of messages.
|
||||||
|
- `[p]ban @Twentysix 7 Continued to spam after told to stop.`
|
||||||
|
This will ban Twentysix and it will delete 7 days worth of messages.
|
||||||
|
|
||||||
A user ID should be provided if the user is not a member of this server.
|
A user ID should be provided if the user is not a member of this server.
|
||||||
|
|
||||||
If days is not a number, it's treated as the first word of the reason.
|
If days is not a number, it's treated as the first word of the reason.
|
||||||
|
Minimum 0 days, maximum 7. If not specified, the defaultdays setting will be used instead.
|
||||||
Minimum 0 days, maximum 7. If not specified, defaultdays setting will be used instead."""
|
"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
if days is None:
|
if days is None:
|
||||||
days = await self.config.guild(guild).default_days()
|
days = await self.config.guild(guild).default_days()
|
||||||
@@ -380,8 +394,15 @@ class KickBanMixin(MixinMeta):
|
|||||||
):
|
):
|
||||||
"""Mass bans user(s) from the server.
|
"""Mass bans user(s) from the server.
|
||||||
|
|
||||||
|
`days` is the amount of days of messages to cleanup on massban.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
- `[p]massban 345628097929936898 57287406247743488 7 they broke all rules.`
|
||||||
|
This will ban all the added userids and delete 7 days of worth messages.
|
||||||
|
|
||||||
User IDs need to be provided in order to ban
|
User IDs need to be provided in order to ban
|
||||||
using this command."""
|
using this command.
|
||||||
|
"""
|
||||||
banned = []
|
banned = []
|
||||||
errors = {}
|
errors = {}
|
||||||
upgrades = []
|
upgrades = []
|
||||||
@@ -543,7 +564,19 @@ class KickBanMixin(MixinMeta):
|
|||||||
*,
|
*,
|
||||||
reason: str = None,
|
reason: str = None,
|
||||||
):
|
):
|
||||||
"""Temporarily ban a user from this server."""
|
"""Temporarily ban a user from this server.
|
||||||
|
|
||||||
|
`duration` is the amount of time the user should be banned for.
|
||||||
|
`days` is the amount of days of messages to cleanup on tempban.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `[p]tempban @Twentysix Because I say so`
|
||||||
|
This will ban Twentysix for the default amount of time set by an administrator.
|
||||||
|
- `[p]tempban @Twentysix 15m You need a timeout`
|
||||||
|
This will ban Twentysix for 15 minutes.
|
||||||
|
- `[p]tempban 428675506947227648 1d2h15m 5 Evil person`
|
||||||
|
This will ban the user for 1 day 2 hours 15 minutes and will delete the last 5 days of their messages.
|
||||||
|
"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
|
|
||||||
|
|||||||
@@ -10,4 +10,4 @@ async def is_allowed_by_hierarchy(
|
|||||||
if not await config.guild(guild).respect_hierarchy():
|
if not await config.guild(guild).respect_hierarchy():
|
||||||
return True
|
return True
|
||||||
is_special = mod == guild.owner or await bot.is_owner(mod)
|
is_special = mod == guild.owner or await bot.is_owner(mod)
|
||||||
return mod.top_role.position > user.top_role.position or is_special
|
return mod.top_role > user.top_role or is_special
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import discord
|
|||||||
from redbot.core import checks, commands, modlog
|
from redbot.core import checks, commands, modlog
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
from redbot.core.i18n import Translator, cog_i18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
from redbot.core.utils.chat_formatting import box
|
from redbot.core.utils.chat_formatting import box, pagify
|
||||||
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
|
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
|
||||||
|
|
||||||
_ = Translator("ModLog", __file__)
|
_ = Translator("ModLog", __file__)
|
||||||
@@ -129,39 +129,76 @@ class ModLog(commands.Cog):
|
|||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def casesfor(self, ctx: commands.Context, *, member: Union[discord.Member, int]):
|
async def casesfor(self, ctx: commands.Context, *, member: Union[discord.Member, int]):
|
||||||
"""Display cases for the specified member."""
|
"""Display cases for the specified member."""
|
||||||
try:
|
async with ctx.typing():
|
||||||
if isinstance(member, int):
|
try:
|
||||||
cases = await modlog.get_cases_for_member(
|
if isinstance(member, int):
|
||||||
bot=ctx.bot, guild=ctx.guild, member_id=member
|
cases = await modlog.get_cases_for_member(
|
||||||
|
bot=ctx.bot, guild=ctx.guild, member_id=member
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cases = await modlog.get_cases_for_member(
|
||||||
|
bot=ctx.bot, guild=ctx.guild, member=member
|
||||||
|
)
|
||||||
|
except discord.NotFound:
|
||||||
|
return await ctx.send(_("That user does not exist."))
|
||||||
|
except discord.HTTPException:
|
||||||
|
return await ctx.send(
|
||||||
|
_("Something unexpected went wrong while fetching that user by ID.")
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
cases = await modlog.get_cases_for_member(
|
if not cases:
|
||||||
bot=ctx.bot, guild=ctx.guild, member=member
|
return await ctx.send(_("That user does not have any cases."))
|
||||||
|
|
||||||
|
embed_requested = await ctx.embed_requested()
|
||||||
|
if embed_requested:
|
||||||
|
rendered_cases = [await case.message_content(embed=True) for case in cases]
|
||||||
|
elif not embed_requested:
|
||||||
|
rendered_cases = []
|
||||||
|
for case in cases:
|
||||||
|
message = _("{case}\n**Timestamp:** {timestamp}").format(
|
||||||
|
case=await case.message_content(embed=False),
|
||||||
|
timestamp=datetime.utcfromtimestamp(case.created_at).strftime(
|
||||||
|
"%Y-%m-%d %H:%M:%S UTC"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rendered_cases.append(message)
|
||||||
|
|
||||||
|
await menu(ctx, rendered_cases, DEFAULT_CONTROLS)
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
@commands.guild_only()
|
||||||
|
async def listcases(self, ctx: commands.Context, *, member: Union[discord.Member, int]):
|
||||||
|
"""List cases for the specified member."""
|
||||||
|
async with ctx.typing():
|
||||||
|
try:
|
||||||
|
if isinstance(member, int):
|
||||||
|
cases = await modlog.get_cases_for_member(
|
||||||
|
bot=ctx.bot, guild=ctx.guild, member_id=member
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cases = await modlog.get_cases_for_member(
|
||||||
|
bot=ctx.bot, guild=ctx.guild, member=member
|
||||||
|
)
|
||||||
|
except discord.NotFound:
|
||||||
|
return await ctx.send(_("That user does not exist."))
|
||||||
|
except discord.HTTPException:
|
||||||
|
return await ctx.send(
|
||||||
|
_("Something unexpected went wrong while fetching that user by ID.")
|
||||||
)
|
)
|
||||||
except discord.NotFound:
|
if not cases:
|
||||||
return await ctx.send(_("That user does not exist."))
|
return await ctx.send(_("That user does not have any cases."))
|
||||||
except discord.HTTPException:
|
|
||||||
return await ctx.send(
|
|
||||||
_("Something unexpected went wrong while fetching that user by ID.")
|
|
||||||
)
|
|
||||||
|
|
||||||
if not cases:
|
|
||||||
return await ctx.send(_("That user does not have any cases."))
|
|
||||||
|
|
||||||
embed_requested = await ctx.embed_requested()
|
|
||||||
if embed_requested:
|
|
||||||
rendered_cases = [await case.message_content(embed=True) for case in cases]
|
|
||||||
elif not embed_requested:
|
|
||||||
rendered_cases = []
|
rendered_cases = []
|
||||||
|
message = ""
|
||||||
for case in cases:
|
for case in cases:
|
||||||
message = _("{case}\n**Timestamp:** {timestamp}").format(
|
message += _("{case}\n**Timestamp:** {timestamp}\n\n").format(
|
||||||
case=await case.message_content(embed=False),
|
case=await case.message_content(embed=False),
|
||||||
timestamp=datetime.utcfromtimestamp(case.created_at).strftime(
|
timestamp=datetime.utcfromtimestamp(case.created_at).strftime(
|
||||||
"%Y-%m-%d %H:%M:%S UTC"
|
"%Y-%m-%d %H:%M:%S UTC"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
rendered_cases.append(message)
|
for page in pagify(message, ["\n\n", "\n"], priority=True):
|
||||||
|
rendered_cases.append(page)
|
||||||
await menu(ctx, rendered_cases, DEFAULT_CONTROLS)
|
await menu(ctx, rendered_cases, DEFAULT_CONTROLS)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import List, Tuple, Optional, Dict
|
from typing import List, Tuple, Optional, Dict, Union
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
@@ -25,3 +25,15 @@ class MixinMeta(ABC):
|
|||||||
ctx: commands.Context, user_voice_state: Optional[discord.VoiceState], **perms: bool
|
ctx: commands.Context, user_voice_state: Optional[discord.VoiceState], **perms: bool
|
||||||
) -> bool:
|
) -> bool:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def _send_dm_notification(
|
||||||
|
self,
|
||||||
|
user: Union[discord.User, discord.Member],
|
||||||
|
moderator: Optional[Union[discord.User, discord.Member]],
|
||||||
|
guild: discord.Guild,
|
||||||
|
mute_type: str,
|
||||||
|
reason: Optional[str],
|
||||||
|
duration=None,
|
||||||
|
):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|||||||
@@ -51,5 +51,5 @@ class MuteTime(Converter):
|
|||||||
time_data[k] = int(v)
|
time_data[k] = int(v)
|
||||||
if time_data:
|
if time_data:
|
||||||
result["duration"] = timedelta(**time_data)
|
result["duration"] = timedelta(**time_data)
|
||||||
result["reason"] = argument
|
result["reason"] = argument.strip()
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from .voicemutes import VoiceMutes
|
|||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
from redbot.core import commands, checks, i18n, modlog, Config
|
from redbot.core import commands, checks, i18n, modlog, Config
|
||||||
from redbot.core.utils import bounded_gather
|
from redbot.core.utils import bounded_gather
|
||||||
from redbot.core.utils.chat_formatting import humanize_timedelta, humanize_list, pagify
|
from redbot.core.utils.chat_formatting import bold, humanize_timedelta, humanize_list, pagify
|
||||||
from redbot.core.utils.mod import get_audit_reason
|
from redbot.core.utils.mod import get_audit_reason
|
||||||
from redbot.core.utils.menus import start_adding_reactions
|
from redbot.core.utils.menus import start_adding_reactions
|
||||||
from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
|
from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
|
||||||
@@ -28,6 +28,9 @@ MUTE_UNMUTE_ISSUES = {
|
|||||||
"hierarchy_problem": _(
|
"hierarchy_problem": _(
|
||||||
"I cannot let you do that. You are not higher than the user in the role hierarchy."
|
"I cannot let you do that. You are not higher than the user in the role hierarchy."
|
||||||
),
|
),
|
||||||
|
"assigned_role_hierarchy_problem": _(
|
||||||
|
"I cannot let you do that. You are not higher than the mute role in the role hierarchy."
|
||||||
|
),
|
||||||
"is_admin": _("That user cannot be muted, as they have the Administrator permission."),
|
"is_admin": _("That user cannot be muted, as they have the Administrator permission."),
|
||||||
"permissions_issue_role": _(
|
"permissions_issue_role": _(
|
||||||
"Failed to mute or unmute user. I need the Manage Roles "
|
"Failed to mute or unmute user. I need the Manage Roles "
|
||||||
@@ -74,6 +77,8 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
|
|||||||
"notification_channel": None,
|
"notification_channel": None,
|
||||||
"muted_users": {},
|
"muted_users": {},
|
||||||
"default_time": 0,
|
"default_time": 0,
|
||||||
|
"dm": False,
|
||||||
|
"show_mod": False,
|
||||||
}
|
}
|
||||||
self.config.register_global(force_role_mutes=True)
|
self.config.register_global(force_role_mutes=True)
|
||||||
# Tbh I would rather force everyone to use role mutes.
|
# Tbh I would rather force everyone to use role mutes.
|
||||||
@@ -145,7 +150,7 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
|
|||||||
self, guild: discord.Guild, mod: discord.Member, user: discord.Member
|
self, guild: discord.Guild, mod: discord.Member, user: discord.Member
|
||||||
):
|
):
|
||||||
is_special = mod == guild.owner or await self.bot.is_owner(mod)
|
is_special = mod == guild.owner or await self.bot.is_owner(mod)
|
||||||
return mod.top_role.position > user.top_role.position or is_special
|
return mod.top_role > user.top_role or is_special
|
||||||
|
|
||||||
async def _handle_automatic_unmute(self):
|
async def _handle_automatic_unmute(self):
|
||||||
"""This is the core task creator and loop
|
"""This is the core task creator and loop
|
||||||
@@ -253,6 +258,9 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
|
|||||||
_("Automatic unmute"),
|
_("Automatic unmute"),
|
||||||
until=None,
|
until=None,
|
||||||
)
|
)
|
||||||
|
await self._send_dm_notification(
|
||||||
|
member, author, guild, _("Server unmute"), _("Automatic unmute")
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
chan_id = await self.config.guild(guild).notification_channel()
|
chan_id = await self.config.guild(guild).notification_channel()
|
||||||
notification_channel = guild.get_channel(chan_id)
|
notification_channel = guild.get_channel(chan_id)
|
||||||
@@ -360,6 +368,9 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
|
|||||||
modlog_reason,
|
modlog_reason,
|
||||||
until=None,
|
until=None,
|
||||||
)
|
)
|
||||||
|
await self._send_dm_notification(
|
||||||
|
member, author, guild, _("Server unmute"), _("Automatic unmute")
|
||||||
|
)
|
||||||
self._channel_mute_events[guild.id].set()
|
self._channel_mute_events[guild.id].set()
|
||||||
if any(results):
|
if any(results):
|
||||||
reasons = {}
|
reasons = {}
|
||||||
@@ -421,8 +432,10 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
|
|||||||
if create_case:
|
if create_case:
|
||||||
if isinstance(channel, discord.VoiceChannel):
|
if isinstance(channel, discord.VoiceChannel):
|
||||||
unmute_type = "vunmute"
|
unmute_type = "vunmute"
|
||||||
|
notification_title = _("Voice unmute")
|
||||||
else:
|
else:
|
||||||
unmute_type = "cunmute"
|
unmute_type = "cunmute"
|
||||||
|
notification_title = _("Channel unmute")
|
||||||
await modlog.create_case(
|
await modlog.create_case(
|
||||||
self.bot,
|
self.bot,
|
||||||
channel.guild,
|
channel.guild,
|
||||||
@@ -434,6 +447,9 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
|
|||||||
until=None,
|
until=None,
|
||||||
channel=channel,
|
channel=channel,
|
||||||
)
|
)
|
||||||
|
await self._send_dm_notification(
|
||||||
|
member, author, channel.guild, notification_title, _("Automatic unmute")
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
error_msg = _(
|
error_msg = _(
|
||||||
@@ -454,6 +470,72 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
|
|||||||
else:
|
else:
|
||||||
return (member, channel, success["reason"])
|
return (member, channel, success["reason"])
|
||||||
|
|
||||||
|
async def _send_dm_notification(
|
||||||
|
self,
|
||||||
|
user: Union[discord.User, discord.Member],
|
||||||
|
moderator: Optional[Union[discord.User, discord.Member]],
|
||||||
|
guild: discord.Guild,
|
||||||
|
mute_type: str,
|
||||||
|
reason: Optional[str],
|
||||||
|
duration=None,
|
||||||
|
):
|
||||||
|
if not await self.config.guild(guild).dm():
|
||||||
|
return
|
||||||
|
|
||||||
|
show_mod = await self.config.guild(guild).show_mod()
|
||||||
|
title = bold(mute_type)
|
||||||
|
if duration:
|
||||||
|
duration_str = humanize_timedelta(timedelta=duration)
|
||||||
|
until = datetime.now(timezone.utc) + duration
|
||||||
|
until_str = until.strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||||
|
|
||||||
|
if moderator is None:
|
||||||
|
moderator_str = _("Unknown")
|
||||||
|
else:
|
||||||
|
moderator_str = str(moderator)
|
||||||
|
|
||||||
|
if not reason:
|
||||||
|
reason = _("No reason provided.")
|
||||||
|
|
||||||
|
# okay, this is some poor API to require PrivateChannel here...
|
||||||
|
if await self.bot.embed_requested(await user.create_dm(), user):
|
||||||
|
em = discord.Embed(
|
||||||
|
title=title,
|
||||||
|
description=reason,
|
||||||
|
color=await self.bot.get_embed_color(user),
|
||||||
|
)
|
||||||
|
em.timestamp = datetime.utcnow()
|
||||||
|
if duration:
|
||||||
|
em.add_field(name=_("Until"), value=until_str)
|
||||||
|
em.add_field(name=_("Duration"), value=duration_str)
|
||||||
|
em.add_field(name=_("Guild"), value=guild.name, inline=False)
|
||||||
|
if show_mod:
|
||||||
|
em.add_field(name=_("Moderator"), value=moderator_str)
|
||||||
|
try:
|
||||||
|
await user.send(embed=em)
|
||||||
|
except discord.Forbidden:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
message = f"{title}\n>>> "
|
||||||
|
message += reason
|
||||||
|
message += (
|
||||||
|
_("\n**Moderator**: {moderator}").format(moderator=moderator_str)
|
||||||
|
if show_mod
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
message += (
|
||||||
|
_("\n**Until**: {until}\n**Duration**: {duration}").format(
|
||||||
|
until=until_str, duration=duration_str
|
||||||
|
)
|
||||||
|
if duration
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
message += _("\n**Guild**: {guild_name}").format(guild_name=guild.name)
|
||||||
|
try:
|
||||||
|
await user.send(message)
|
||||||
|
except discord.Forbidden:
|
||||||
|
pass
|
||||||
|
|
||||||
@commands.Cog.listener()
|
@commands.Cog.listener()
|
||||||
async def on_member_update(self, before: discord.Member, after: discord.Member):
|
async def on_member_update(self, before: discord.Member, after: discord.Member):
|
||||||
"""
|
"""
|
||||||
@@ -491,6 +573,9 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
|
|||||||
)
|
)
|
||||||
del self._server_mutes[guild.id][after.id]
|
del self._server_mutes[guild.id][after.id]
|
||||||
should_save = True
|
should_save = True
|
||||||
|
await self._send_dm_notification(
|
||||||
|
after, None, guild, _("Server unmute"), _("Manually removed mute role")
|
||||||
|
)
|
||||||
elif mute_role in roles_added:
|
elif mute_role in roles_added:
|
||||||
# send modlog case for mute and add to cache
|
# send modlog case for mute and add to cache
|
||||||
if guild.id not in self._server_mutes:
|
if guild.id not in self._server_mutes:
|
||||||
@@ -512,6 +597,9 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
|
|||||||
"until": None,
|
"until": None,
|
||||||
}
|
}
|
||||||
should_save = True
|
should_save = True
|
||||||
|
await self._send_dm_notification(
|
||||||
|
after, None, guild, _("Server mute"), _("Manually applied mute role")
|
||||||
|
)
|
||||||
if should_save:
|
if should_save:
|
||||||
await self.config.guild(guild).muted_users.set(self._server_mutes[guild.id])
|
await self.config.guild(guild).muted_users.set(self._server_mutes[guild.id])
|
||||||
|
|
||||||
@@ -552,15 +640,27 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
|
|||||||
user_id not in after_perms or any((send_messages, speak))
|
user_id not in after_perms or any((send_messages, speak))
|
||||||
):
|
):
|
||||||
user = after.guild.get_member(user_id)
|
user = after.guild.get_member(user_id)
|
||||||
|
send_dm_notification = True
|
||||||
if not user:
|
if not user:
|
||||||
|
send_dm_notification = False
|
||||||
user = discord.Object(id=user_id)
|
user = discord.Object(id=user_id)
|
||||||
log.debug(f"{user} - {type(user)}")
|
log.debug(f"{user} - {type(user)}")
|
||||||
to_del.append(user_id)
|
to_del.append(user_id)
|
||||||
log.debug("creating case")
|
log.debug("creating case")
|
||||||
if isinstance(after, discord.VoiceChannel):
|
if isinstance(after, discord.VoiceChannel):
|
||||||
unmute_type = "vunmute"
|
unmute_type = "vunmute"
|
||||||
|
notification_title = _("Voice unmute")
|
||||||
else:
|
else:
|
||||||
unmute_type = "cunmute"
|
unmute_type = "cunmute"
|
||||||
|
notification_title = _("Channel unmute")
|
||||||
|
if send_dm_notification:
|
||||||
|
await self._send_dm_notification(
|
||||||
|
user,
|
||||||
|
None,
|
||||||
|
after.guild,
|
||||||
|
notification_title,
|
||||||
|
_("Manually removed channel overwrites"),
|
||||||
|
)
|
||||||
await modlog.create_case(
|
await modlog.create_case(
|
||||||
self.bot,
|
self.bot,
|
||||||
after.guild,
|
after.guild,
|
||||||
@@ -611,6 +711,34 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
|
|||||||
"""Mute settings."""
|
"""Mute settings."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@muteset.command()
|
||||||
|
@commands.guild_only()
|
||||||
|
async def senddm(self, ctx: commands.Context, true_or_false: bool):
|
||||||
|
"""Set whether mute notifications should be sent to users in DMs."""
|
||||||
|
await self.config.guild(ctx.guild).dm.set(true_or_false)
|
||||||
|
if true_or_false:
|
||||||
|
await ctx.send(_("I will now try to send mute notifications to users DMs."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Mute notifications will no longer be sent to users DMs."))
|
||||||
|
|
||||||
|
@muteset.command()
|
||||||
|
@commands.guild_only()
|
||||||
|
async def showmoderator(self, ctx, true_or_false: bool):
|
||||||
|
"""Decide whether the name of the moderator muting a user should be included in the DM to that user."""
|
||||||
|
await self.config.guild(ctx.guild).show_mod.set(true_or_false)
|
||||||
|
if true_or_false:
|
||||||
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"I will include the name of the moderator who issued the mute when sending a DM to a user."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"I will not include the name of the moderator who issued the mute when sending a DM to a user."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@muteset.command(name="forcerole")
|
@muteset.command(name="forcerole")
|
||||||
@commands.is_owner()
|
@commands.is_owner()
|
||||||
async def force_role_mutes(self, ctx: commands.Context, force_role_mutes: bool):
|
async def force_role_mutes(self, ctx: commands.Context, force_role_mutes: bool):
|
||||||
@@ -635,11 +763,17 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
|
|||||||
notification_channel = ctx.guild.get_channel(data["notification_channel"])
|
notification_channel = ctx.guild.get_channel(data["notification_channel"])
|
||||||
default_time = timedelta(seconds=data["default_time"])
|
default_time = timedelta(seconds=data["default_time"])
|
||||||
msg = _(
|
msg = _(
|
||||||
"Mute Role: {role}\nNotification Channel: {channel}\n" "Default Time: {time}"
|
"Mute Role: {role}\n"
|
||||||
|
"Notification Channel: {channel}\n"
|
||||||
|
"Default Time: {time}\n"
|
||||||
|
"Send DM: {dm}\n"
|
||||||
|
"Show moderator: {show_mod}"
|
||||||
).format(
|
).format(
|
||||||
role=mute_role.mention if mute_role else _("None"),
|
role=mute_role.mention if mute_role else _("None"),
|
||||||
channel=notification_channel.mention if notification_channel else _("None"),
|
channel=notification_channel.mention if notification_channel else _("None"),
|
||||||
time=humanize_timedelta(timedelta=default_time) if default_time else _("None"),
|
time=humanize_timedelta(timedelta=default_time) if default_time else _("None"),
|
||||||
|
dm=data["dm"],
|
||||||
|
show_mod=data["show_mod"],
|
||||||
)
|
)
|
||||||
await ctx.maybe_send_embed(msg)
|
await ctx.maybe_send_embed(msg)
|
||||||
|
|
||||||
@@ -684,6 +818,11 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
|
|||||||
# removed the mute role
|
# removed the mute role
|
||||||
await ctx.send(_("Channel overwrites will be used for mutes instead."))
|
await ctx.send(_("Channel overwrites will be used for mutes instead."))
|
||||||
else:
|
else:
|
||||||
|
if role >= ctx.author.top_role:
|
||||||
|
await ctx.send(
|
||||||
|
_("You can't set this role as it is not lower than you in the role hierarchy.")
|
||||||
|
)
|
||||||
|
return
|
||||||
await self.config.guild(ctx.guild).mute_role.set(role.id)
|
await self.config.guild(ctx.guild).mute_role.set(role.id)
|
||||||
self.mute_role_cache[ctx.guild.id] = role.id
|
self.mute_role_cache[ctx.guild.id] = role.id
|
||||||
await ctx.send(_("Mute role set to {role}").format(role=role.name))
|
await ctx.send(_("Mute role set to {role}").format(role=role.name))
|
||||||
@@ -999,6 +1138,9 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
|
|||||||
until=until,
|
until=until,
|
||||||
channel=None,
|
channel=None,
|
||||||
)
|
)
|
||||||
|
await self._send_dm_notification(
|
||||||
|
user, author, guild, _("Server mute"), reason, duration
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
issue_list.append(success)
|
issue_list.append(success)
|
||||||
if success_list:
|
if success_list:
|
||||||
@@ -1143,6 +1285,9 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
|
|||||||
until=until,
|
until=until,
|
||||||
channel=channel,
|
channel=channel,
|
||||||
)
|
)
|
||||||
|
await self._send_dm_notification(
|
||||||
|
user, author, guild, _("Channel mute"), reason, duration
|
||||||
|
)
|
||||||
async with self.config.member(user).perms_cache() as cache:
|
async with self.config.member(user).perms_cache() as cache:
|
||||||
cache[channel.id] = success["old_overs"]
|
cache[channel.id] = success["old_overs"]
|
||||||
else:
|
else:
|
||||||
@@ -1209,6 +1354,9 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
|
|||||||
reason,
|
reason,
|
||||||
until=None,
|
until=None,
|
||||||
)
|
)
|
||||||
|
await self._send_dm_notification(
|
||||||
|
user, author, guild, _("Server unmute"), reason
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
issue_list.append(success)
|
issue_list.append(success)
|
||||||
self._channel_mute_events[guild.id].set()
|
self._channel_mute_events[guild.id].set()
|
||||||
@@ -1273,6 +1421,9 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
|
|||||||
until=None,
|
until=None,
|
||||||
channel=channel,
|
channel=channel,
|
||||||
)
|
)
|
||||||
|
await self._send_dm_notification(
|
||||||
|
user, author, guild, _("Channel unmute"), reason
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
issue_list.append((user, success["reason"]))
|
issue_list.append((user, success["reason"]))
|
||||||
if success_list:
|
if success_list:
|
||||||
@@ -1331,6 +1482,9 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
|
|||||||
if not role:
|
if not role:
|
||||||
ret["reason"] = _(MUTE_UNMUTE_ISSUES["role_missing"])
|
ret["reason"] = _(MUTE_UNMUTE_ISSUES["role_missing"])
|
||||||
return ret
|
return ret
|
||||||
|
if author != guild.owner and role >= author.top_role:
|
||||||
|
ret["reason"] = _(MUTE_UNMUTE_ISSUES["assigned_role_hierarchy_problem"])
|
||||||
|
return ret
|
||||||
if not guild.me.guild_permissions.manage_roles or role >= guild.me.top_role:
|
if not guild.me.guild_permissions.manage_roles or role >= guild.me.top_role:
|
||||||
ret["reason"] = _(MUTE_UNMUTE_ISSUES["permissions_issue_role"])
|
ret["reason"] = _(MUTE_UNMUTE_ISSUES["permissions_issue_role"])
|
||||||
return ret
|
return ret
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple, Union
|
||||||
from datetime import timezone, timedelta, datetime
|
from datetime import timezone, timedelta, datetime
|
||||||
from .abc import MixinMeta
|
from .abc import MixinMeta
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from redbot.core import commands, checks, i18n, modlog
|
from redbot.core import commands, checks, i18n, modlog
|
||||||
from redbot.core.utils.chat_formatting import (
|
from redbot.core.utils.chat_formatting import (
|
||||||
|
bold,
|
||||||
humanize_timedelta,
|
humanize_timedelta,
|
||||||
humanize_list,
|
humanize_list,
|
||||||
pagify,
|
pagify,
|
||||||
@@ -143,6 +144,9 @@ class VoiceMutes(MixinMeta):
|
|||||||
until=until,
|
until=until,
|
||||||
channel=channel,
|
channel=channel,
|
||||||
)
|
)
|
||||||
|
await self._send_dm_notification(
|
||||||
|
user, author, guild, _("Voice mute"), reason, duration
|
||||||
|
)
|
||||||
async with self.config.member(user).perms_cache() as cache:
|
async with self.config.member(user).perms_cache() as cache:
|
||||||
cache[channel.id] = success["old_overs"]
|
cache[channel.id] = success["old_overs"]
|
||||||
else:
|
else:
|
||||||
@@ -216,6 +220,9 @@ class VoiceMutes(MixinMeta):
|
|||||||
until=None,
|
until=None,
|
||||||
channel=channel,
|
channel=channel,
|
||||||
)
|
)
|
||||||
|
await self._send_dm_notification(
|
||||||
|
user, author, guild, _("Voice unmute"), reason
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
issue_list.append((user, success["reason"]))
|
issue_list.append((user, success["reason"]))
|
||||||
if success_list:
|
if success_list:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import logging
|
|||||||
from dateutil.parser import parse as parse_time
|
from dateutil.parser import parse as parse_time
|
||||||
from random import choice
|
from random import choice
|
||||||
from string import ascii_letters
|
from string import ascii_letters
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, timezone
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from typing import ClassVar, Optional, List, Tuple
|
from typing import ClassVar, Optional, List, Tuple
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ YOUTUBE_CHANNEL_RSS = "https://www.youtube.com/feeds/videos.xml?channel_id={chan
|
|||||||
|
|
||||||
_ = Translator("Streams", __file__)
|
_ = Translator("Streams", __file__)
|
||||||
|
|
||||||
log = logging.getLogger("redbot.cogs.Streams")
|
log = logging.getLogger("red.core.cogs.Streams")
|
||||||
|
|
||||||
|
|
||||||
def rnd(url):
|
def rnd(url):
|
||||||
@@ -138,9 +138,7 @@ class YoutubeStream(Stream):
|
|||||||
scheduled = stream_data.get("scheduledStartTime", None)
|
scheduled = stream_data.get("scheduledStartTime", None)
|
||||||
if scheduled is not None and actual_start_time is None:
|
if scheduled is not None and actual_start_time is None:
|
||||||
scheduled = parse_time(scheduled)
|
scheduled = parse_time(scheduled)
|
||||||
if (
|
if (scheduled - datetime.now(timezone.utc)).total_seconds() < -3600:
|
||||||
scheduled.replace(tzinfo=None) - datetime.now()
|
|
||||||
).total_seconds() < -3600:
|
|
||||||
continue
|
continue
|
||||||
elif actual_start_time is None:
|
elif actual_start_time is None:
|
||||||
continue
|
continue
|
||||||
@@ -178,7 +176,7 @@ class YoutubeStream(Stream):
|
|||||||
if vid_data["liveStreamingDetails"].get("scheduledStartTime", None) is not None:
|
if vid_data["liveStreamingDetails"].get("scheduledStartTime", None) is not None:
|
||||||
if "actualStartTime" not in vid_data["liveStreamingDetails"]:
|
if "actualStartTime" not in vid_data["liveStreamingDetails"]:
|
||||||
start_time = parse_time(vid_data["liveStreamingDetails"]["scheduledStartTime"])
|
start_time = parse_time(vid_data["liveStreamingDetails"]["scheduledStartTime"])
|
||||||
start_in = start_time.replace(tzinfo=None) - datetime.now()
|
start_in = start_time - datetime.now(timezone.utc)
|
||||||
if start_in.total_seconds() > 0:
|
if start_in.total_seconds() > 0:
|
||||||
embed.description = _("This stream will start in {time}").format(
|
embed.description = _("This stream will start in {time}").format(
|
||||||
time=humanize_timedelta(
|
time=humanize_timedelta(
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ class RedBase(
|
|||||||
help__delete_delay=0,
|
help__delete_delay=0,
|
||||||
help__use_menus=False,
|
help__use_menus=False,
|
||||||
help__show_hidden=False,
|
help__show_hidden=False,
|
||||||
|
help__show_aliases=True,
|
||||||
help__verify_checks=True,
|
help__verify_checks=True,
|
||||||
help__verify_exists=False,
|
help__verify_exists=False,
|
||||||
help__tagline="",
|
help__tagline="",
|
||||||
@@ -281,6 +282,92 @@ class RedBase(
|
|||||||
"""
|
"""
|
||||||
self._help_formatter = commands.help.RedHelpFormatter()
|
self._help_formatter = commands.help.RedHelpFormatter()
|
||||||
|
|
||||||
|
def add_dev_env_value(self, name: str, value: Callable[[commands.Context], Any]):
|
||||||
|
"""
|
||||||
|
Add a custom variable to the dev environment (``[p]debug``, ``[p]eval``, and ``[p]repl`` commands).
|
||||||
|
If dev mode is disabled, nothing will happen.
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class MyCog(commands.Cog):
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
bot.add_dev_env_value("mycog", lambda ctx: self)
|
||||||
|
bot.add_dev_env_value("mycogdata", lambda ctx: self.settings[ctx.guild.id])
|
||||||
|
|
||||||
|
def cog_unload(self):
|
||||||
|
self.bot.remove_dev_env_value("mycog")
|
||||||
|
self.bot.remove_dev_env_value("mycogdata")
|
||||||
|
|
||||||
|
Once your cog is loaded, the custom variables ``mycog`` and ``mycogdata``
|
||||||
|
will be included in the environment of dev commands.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
name: str
|
||||||
|
The name of your custom variable.
|
||||||
|
value: Callable[[commands.Context], Any]
|
||||||
|
The function returning the value of the variable.
|
||||||
|
It must take a `commands.Context` as its sole parameter
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
TypeError
|
||||||
|
``value`` argument isn't a callable.
|
||||||
|
ValueError
|
||||||
|
The passed callable takes no or more than one argument.
|
||||||
|
RuntimeError
|
||||||
|
The name of the custom variable is either reserved by a variable
|
||||||
|
from the default environment or already taken by some other custom variable.
|
||||||
|
"""
|
||||||
|
signature = inspect.signature(value)
|
||||||
|
if len(signature.parameters) != 1:
|
||||||
|
raise ValueError("Callable must take exactly one argument for context")
|
||||||
|
dev = self.get_cog("Dev")
|
||||||
|
if dev is None:
|
||||||
|
return
|
||||||
|
if name in [
|
||||||
|
"bot",
|
||||||
|
"ctx",
|
||||||
|
"channel",
|
||||||
|
"author",
|
||||||
|
"guild",
|
||||||
|
"message",
|
||||||
|
"asyncio",
|
||||||
|
"aiohttp",
|
||||||
|
"discord",
|
||||||
|
"commands",
|
||||||
|
"_",
|
||||||
|
"__name__",
|
||||||
|
"__builtins__",
|
||||||
|
]:
|
||||||
|
raise RuntimeError(f"The name {name} is reserved for default environement.")
|
||||||
|
if name in dev.env_extensions:
|
||||||
|
raise RuntimeError(f"The name {name} is already used.")
|
||||||
|
dev.env_extensions[name] = value
|
||||||
|
|
||||||
|
def remove_dev_env_value(self, name: str):
|
||||||
|
"""
|
||||||
|
Remove a custom variable from the dev environment.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
name: str
|
||||||
|
The name of the custom variable.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
KeyError
|
||||||
|
The custom variable was never set.
|
||||||
|
"""
|
||||||
|
dev = self.get_cog("Dev")
|
||||||
|
if dev is None:
|
||||||
|
return
|
||||||
|
del dev.env_extensions[name]
|
||||||
|
|
||||||
def get_command(self, name: str) -> Optional[commands.Command]:
|
def get_command(self, name: str) -> Optional[commands.Command]:
|
||||||
com = super().get_command(name)
|
com = super().get_command(name)
|
||||||
assert com is None or isinstance(com, commands.Command)
|
assert com is None or isinstance(com, commands.Command)
|
||||||
@@ -383,6 +470,12 @@ class RedBase(
|
|||||||
self._red_before_invoke_objs.add(coro)
|
self._red_before_invoke_objs.add(coro)
|
||||||
return coro
|
return coro
|
||||||
|
|
||||||
|
async def before_identify_hook(self, shard_id, *, initial=False):
|
||||||
|
"""A hook that is called before IDENTIFYing a session.
|
||||||
|
Same as in discord.py, but also dispatches "on_red_identify" bot event."""
|
||||||
|
self.dispatch("red_before_identify", shard_id, initial)
|
||||||
|
return await super().before_identify_hook(shard_id, initial=initial)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cog_mgr(self) -> NoReturn:
|
def cog_mgr(self) -> NoReturn:
|
||||||
raise AttributeError("Please don't mess with the cog manager internals.")
|
raise AttributeError("Please don't mess with the cog manager internals.")
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ from ..i18n import Translator
|
|||||||
from ..utils import menus
|
from ..utils import menus
|
||||||
from ..utils.mod import mass_purge
|
from ..utils.mod import mass_purge
|
||||||
from ..utils._internal_utils import fuzzy_command_search, format_fuzzy_results
|
from ..utils._internal_utils import fuzzy_command_search, format_fuzzy_results
|
||||||
from ..utils.chat_formatting import box, pagify, humanize_timedelta
|
from ..utils.chat_formatting import box, humanize_list, humanize_number, humanize_timedelta, pagify
|
||||||
|
|
||||||
__all__ = ["red_help", "RedHelpFormatter", "HelpSettings", "HelpFormatterABC"]
|
__all__ = ["red_help", "RedHelpFormatter", "HelpSettings", "HelpFormatterABC"]
|
||||||
|
|
||||||
@@ -72,6 +72,7 @@ class HelpSettings:
|
|||||||
max_pages_in_guild: int = 2
|
max_pages_in_guild: int = 2
|
||||||
use_menus: bool = False
|
use_menus: bool = False
|
||||||
show_hidden: bool = False
|
show_hidden: bool = False
|
||||||
|
show_aliases: bool = True
|
||||||
verify_checks: bool = True
|
verify_checks: bool = True
|
||||||
verify_exists: bool = False
|
verify_exists: bool = False
|
||||||
tagline: str = ""
|
tagline: str = ""
|
||||||
@@ -300,10 +301,42 @@ class RedHelpFormatter(HelpFormatterABC):
|
|||||||
|
|
||||||
tagline = (help_settings.tagline) or self.get_default_tagline(ctx)
|
tagline = (help_settings.tagline) or self.get_default_tagline(ctx)
|
||||||
signature = _(
|
signature = _(
|
||||||
"`Syntax: {ctx.clean_prefix}{command.qualified_name} {command.signature}`"
|
"Syntax: {ctx.clean_prefix}{command.qualified_name} {command.signature}"
|
||||||
).format(ctx=ctx, command=command)
|
).format(ctx=ctx, command=command)
|
||||||
subcommands = None
|
|
||||||
|
|
||||||
|
aliases = command.aliases
|
||||||
|
if help_settings.show_aliases and aliases:
|
||||||
|
alias_fmt = _("Aliases") if len(command.aliases) > 1 else _("Alias")
|
||||||
|
aliases = sorted(aliases, key=len)
|
||||||
|
|
||||||
|
a_counter = 0
|
||||||
|
valid_alias_list = []
|
||||||
|
for alias in aliases:
|
||||||
|
if (a_counter := a_counter + len(alias)) < 500:
|
||||||
|
valid_alias_list.append(alias)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
a_diff = len(aliases) - len(valid_alias_list)
|
||||||
|
aliases_list = [
|
||||||
|
f"{ctx.clean_prefix}{command.parent.qualified_name + ' ' if command.parent else ''}{alias}"
|
||||||
|
for alias in valid_alias_list
|
||||||
|
]
|
||||||
|
if len(valid_alias_list) < 10:
|
||||||
|
aliases_content = humanize_list(aliases_list)
|
||||||
|
else:
|
||||||
|
aliases_formatted_list = ", ".join(aliases_list)
|
||||||
|
if a_diff > 1:
|
||||||
|
aliases_content = _("{aliases} and {number} more aliases.").format(
|
||||||
|
aliases=aliases_formatted_list, number=humanize_number(a_diff)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
aliases_content = _("{aliases} and one more alias.").format(
|
||||||
|
aliases=aliases_formatted_list
|
||||||
|
)
|
||||||
|
signature += f"\n{alias_fmt}: {aliases_content}"
|
||||||
|
|
||||||
|
subcommands = None
|
||||||
if hasattr(command, "all_commands"):
|
if hasattr(command, "all_commands"):
|
||||||
grp = cast(commands.Group, command)
|
grp = cast(commands.Group, command)
|
||||||
subcommands = await self.get_group_help_mapping(ctx, grp, help_settings=help_settings)
|
subcommands = await self.get_group_help_mapping(ctx, grp, help_settings=help_settings)
|
||||||
@@ -315,7 +348,7 @@ class RedHelpFormatter(HelpFormatterABC):
|
|||||||
emb["embed"]["title"] = f"*{description[:250]}*"
|
emb["embed"]["title"] = f"*{description[:250]}*"
|
||||||
|
|
||||||
emb["footer"]["text"] = tagline
|
emb["footer"]["text"] = tagline
|
||||||
emb["embed"]["description"] = signature
|
emb["embed"]["description"] = box(signature)
|
||||||
|
|
||||||
command_help = command.format_help_for_context(ctx)
|
command_help = command.format_help_for_context(ctx)
|
||||||
if command_help:
|
if command_help:
|
||||||
@@ -375,7 +408,7 @@ class RedHelpFormatter(HelpFormatterABC):
|
|||||||
None,
|
None,
|
||||||
(
|
(
|
||||||
description,
|
description,
|
||||||
signature[1:-1],
|
signature,
|
||||||
command.format_help_for_context(ctx),
|
command.format_help_for_context(ctx),
|
||||||
subtext_header,
|
subtext_header,
|
||||||
subtext,
|
subtext,
|
||||||
|
|||||||
@@ -172,6 +172,20 @@ class CoreLogic:
|
|||||||
except errors.CogLoadError as e:
|
except errors.CogLoadError as e:
|
||||||
failed_with_reason_packages.append((name, str(e)))
|
failed_with_reason_packages.append((name, str(e)))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if isinstance(e, commands.CommandRegistrationError):
|
||||||
|
if e.alias_conflict:
|
||||||
|
error_message = _(
|
||||||
|
"Alias {alias_name} is already an existing command"
|
||||||
|
" or alias in one of the loaded cogs."
|
||||||
|
).format(alias_name=inline(e.name))
|
||||||
|
else:
|
||||||
|
error_message = _(
|
||||||
|
"Command {command_name} is already an existing command"
|
||||||
|
" or alias in one of the loaded cogs."
|
||||||
|
).format(command_name=inline(e.name))
|
||||||
|
failed_with_reason_packages.append((name, error_message))
|
||||||
|
continue
|
||||||
|
|
||||||
log.exception("Package loading failed", exc_info=e)
|
log.exception("Package loading failed", exc_info=e)
|
||||||
|
|
||||||
exception_log = "Exception during loading of package\n"
|
exception_log = "Exception during loading of package\n"
|
||||||
@@ -2094,7 +2108,7 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
|||||||
|
|
||||||
if game:
|
if game:
|
||||||
if len(game) > 128:
|
if len(game) > 128:
|
||||||
await ctx.send("The maximum length of game descriptions is 128 characters.")
|
await ctx.send(_("The maximum length of game descriptions is 128 characters."))
|
||||||
return
|
return
|
||||||
game = discord.Game(name=game)
|
game = discord.Game(name=game)
|
||||||
else:
|
else:
|
||||||
@@ -2126,6 +2140,11 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
|||||||
|
|
||||||
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
|
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
|
||||||
if listening:
|
if listening:
|
||||||
|
if len(listening) > 128:
|
||||||
|
await ctx.send(
|
||||||
|
_("The maximum length of listening descriptions is 128 characters.")
|
||||||
|
)
|
||||||
|
return
|
||||||
activity = discord.Activity(name=listening, type=discord.ActivityType.listening)
|
activity = discord.Activity(name=listening, type=discord.ActivityType.listening)
|
||||||
else:
|
else:
|
||||||
activity = None
|
activity = None
|
||||||
@@ -2157,6 +2176,9 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
|||||||
|
|
||||||
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
|
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
|
||||||
if watching:
|
if watching:
|
||||||
|
if len(watching) > 128:
|
||||||
|
await ctx.send(_("The maximum length of watching descriptions is 128 characters."))
|
||||||
|
return
|
||||||
activity = discord.Activity(name=watching, type=discord.ActivityType.watching)
|
activity = discord.Activity(name=watching, type=discord.ActivityType.watching)
|
||||||
else:
|
else:
|
||||||
activity = None
|
activity = None
|
||||||
@@ -2186,6 +2208,11 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
|||||||
|
|
||||||
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
|
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
|
||||||
if competing:
|
if competing:
|
||||||
|
if len(competing) > 128:
|
||||||
|
await ctx.send(
|
||||||
|
_("The maximum length of competing descriptions is 128 characters.")
|
||||||
|
)
|
||||||
|
return
|
||||||
activity = discord.Activity(name=competing, type=discord.ActivityType.competing)
|
activity = discord.Activity(name=competing, type=discord.ActivityType.competing)
|
||||||
else:
|
else:
|
||||||
activity = None
|
activity = None
|
||||||
@@ -2234,11 +2261,13 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
|||||||
await ctx.bot.change_presence(status=status, activity=game)
|
await ctx.bot.change_presence(status=status, activity=game)
|
||||||
await ctx.send(_("Status changed to {}.").format(status))
|
await ctx.send(_("Status changed to {}.").format(status))
|
||||||
|
|
||||||
@_set.command(name="streaming", aliases=["stream"], usage="[(<streamer> <stream_title>)]")
|
@_set.command(
|
||||||
|
name="streaming", aliases=["stream", "twitch"], usage="[(<streamer> <stream_title>)]"
|
||||||
|
)
|
||||||
@checks.bot_in_a_guild()
|
@checks.bot_in_a_guild()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def stream(self, ctx: commands.Context, streamer=None, *, stream_title=None):
|
async def stream(self, ctx: commands.Context, streamer=None, *, stream_title=None):
|
||||||
"""Sets [botname]'s streaming status.
|
"""Sets [botname]'s streaming status to a twitch stream.
|
||||||
|
|
||||||
This will appear as `Streaming <stream_title>` or `LIVE ON TWITCH` depending on the context.
|
This will appear as `Streaming <stream_title>` or `LIVE ON TWITCH` depending on the context.
|
||||||
It will also include a `Watch` button with a twitch.tv url for the provided streamer.
|
It will also include a `Watch` button with a twitch.tv url for the provided streamer.
|
||||||
@@ -2262,6 +2291,12 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
|||||||
stream_title = stream_title.strip()
|
stream_title = stream_title.strip()
|
||||||
if "twitch.tv/" not in streamer:
|
if "twitch.tv/" not in streamer:
|
||||||
streamer = "https://www.twitch.tv/" + streamer
|
streamer = "https://www.twitch.tv/" + streamer
|
||||||
|
if len(streamer) > 511:
|
||||||
|
await ctx.send(_("The maximum length of the streamer url is 511 characters."))
|
||||||
|
return
|
||||||
|
if len(stream_title) > 128:
|
||||||
|
await ctx.send(_("The maximum length of the stream title is 128 characters."))
|
||||||
|
return
|
||||||
activity = discord.Streaming(url=streamer, name=stream_title)
|
activity = discord.Streaming(url=streamer, name=stream_title)
|
||||||
await ctx.bot.change_presence(status=status, activity=activity)
|
await ctx.bot.change_presence(status=status, activity=activity)
|
||||||
elif streamer is not None:
|
elif streamer is not None:
|
||||||
@@ -2782,6 +2817,22 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
|||||||
else:
|
else:
|
||||||
await ctx.send(_("Help will filter hidden commands."))
|
await ctx.send(_("Help will filter hidden commands."))
|
||||||
|
|
||||||
|
@helpset.command(name="showaliases")
|
||||||
|
async def helpset_showaliases(self, ctx: commands.Context, show_aliases: bool = None):
|
||||||
|
"""
|
||||||
|
This allows the help command to show existing commands aliases if there is any.
|
||||||
|
|
||||||
|
This defaults to True.
|
||||||
|
Using this without a setting will toggle.
|
||||||
|
"""
|
||||||
|
if show_aliases is None:
|
||||||
|
show_aliases = not await ctx.bot._config.help.show_aliases()
|
||||||
|
await ctx.bot._config.help.show_aliases.set(show_aliases)
|
||||||
|
if show_aliases:
|
||||||
|
await ctx.send(_("Help will show commands aliases."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Help will not show commands aliases."))
|
||||||
|
|
||||||
@helpset.command(name="usetick")
|
@helpset.command(name="usetick")
|
||||||
async def helpset_usetick(self, ctx: commands.Context, use_tick: bool = None):
|
async def helpset_usetick(self, ctx: commands.Context, use_tick: bool = None):
|
||||||
"""
|
"""
|
||||||
@@ -3275,8 +3326,10 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
|||||||
"""
|
"""
|
||||||
uids = {getattr(user, "id", user) for user in users}
|
uids = {getattr(user, "id", user) for user in users}
|
||||||
await self.bot._whiteblacklist_cache.add_to_whitelist(None, uids)
|
await self.bot._whiteblacklist_cache.add_to_whitelist(None, uids)
|
||||||
|
if len(users) > 1:
|
||||||
await ctx.send(_("Users added to allowlist."))
|
await ctx.send(_("Users have been added to the allowlist."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("User has been added to the allowlist."))
|
||||||
|
|
||||||
@allowlist.command(name="list")
|
@allowlist.command(name="list")
|
||||||
async def allowlist_list(self, ctx: commands.Context):
|
async def allowlist_list(self, ctx: commands.Context):
|
||||||
@@ -3291,8 +3344,10 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
|||||||
if not curr_list:
|
if not curr_list:
|
||||||
await ctx.send("Allowlist is empty.")
|
await ctx.send("Allowlist is empty.")
|
||||||
return
|
return
|
||||||
|
if len(curr_list) > 1:
|
||||||
msg = _("Users on allowlist:")
|
msg = _("Users on the allowlist:")
|
||||||
|
else:
|
||||||
|
msg = _("User on the allowlist:")
|
||||||
for user in curr_list:
|
for user in curr_list:
|
||||||
msg += "\n\t- {}".format(user)
|
msg += "\n\t- {}".format(user)
|
||||||
|
|
||||||
@@ -3316,8 +3371,10 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
|||||||
"""
|
"""
|
||||||
uids = {getattr(user, "id", user) for user in users}
|
uids = {getattr(user, "id", user) for user in users}
|
||||||
await self.bot._whiteblacklist_cache.remove_from_whitelist(None, uids)
|
await self.bot._whiteblacklist_cache.remove_from_whitelist(None, uids)
|
||||||
|
if len(users) > 1:
|
||||||
await ctx.send(_("Users have been removed from the allowlist."))
|
await ctx.send(_("Users have been removed from the allowlist."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("User has been removed from the allowlist."))
|
||||||
|
|
||||||
@allowlist.command(name="clear")
|
@allowlist.command(name="clear")
|
||||||
async def allowlist_clear(self, ctx: commands.Context):
|
async def allowlist_clear(self, ctx: commands.Context):
|
||||||
@@ -3366,8 +3423,10 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
|||||||
|
|
||||||
uids = {getattr(user, "id", user) for user in users}
|
uids = {getattr(user, "id", user) for user in users}
|
||||||
await self.bot._whiteblacklist_cache.add_to_blacklist(None, uids)
|
await self.bot._whiteblacklist_cache.add_to_blacklist(None, uids)
|
||||||
|
if len(users) > 1:
|
||||||
await ctx.send(_("User added to blocklist."))
|
await ctx.send(_("Users have been added to the blocklist."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("User has been added to the blocklist."))
|
||||||
|
|
||||||
@blocklist.command(name="list")
|
@blocklist.command(name="list")
|
||||||
async def blocklist_list(self, ctx: commands.Context):
|
async def blocklist_list(self, ctx: commands.Context):
|
||||||
@@ -3382,8 +3441,10 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
|||||||
if not curr_list:
|
if not curr_list:
|
||||||
await ctx.send("Blocklist is empty.")
|
await ctx.send("Blocklist is empty.")
|
||||||
return
|
return
|
||||||
|
if len(curr_list) > 1:
|
||||||
msg = _("Users on blocklist:")
|
msg = _("Users on the blocklist:")
|
||||||
|
else:
|
||||||
|
msg = _("User on the blocklist:")
|
||||||
for user in curr_list:
|
for user in curr_list:
|
||||||
msg += "\n\t- {}".format(user)
|
msg += "\n\t- {}".format(user)
|
||||||
|
|
||||||
@@ -3405,8 +3466,10 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
|||||||
"""
|
"""
|
||||||
uids = {getattr(user, "id", user) for user in users}
|
uids = {getattr(user, "id", user) for user in users}
|
||||||
await self.bot._whiteblacklist_cache.remove_from_blacklist(None, uids)
|
await self.bot._whiteblacklist_cache.remove_from_blacklist(None, uids)
|
||||||
|
if len(users) > 1:
|
||||||
await ctx.send(_("Users have been removed from blocklist."))
|
await ctx.send(_("Users have been removed from the blocklist."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("User has been removed from the blocklist."))
|
||||||
|
|
||||||
@blocklist.command(name="clear")
|
@blocklist.command(name="clear")
|
||||||
async def blocklist_clear(self, ctx: commands.Context):
|
async def blocklist_clear(self, ctx: commands.Context):
|
||||||
@@ -3468,7 +3531,7 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
|||||||
@localallowlist.command(name="list")
|
@localallowlist.command(name="list")
|
||||||
async def localallowlist_list(self, ctx: commands.Context):
|
async def localallowlist_list(self, ctx: commands.Context):
|
||||||
"""
|
"""
|
||||||
Lists users and roles on the server allowlist.
|
Lists users and roles on the server allowlist.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
- `[p]localallowlist list`
|
- `[p]localallowlist list`
|
||||||
@@ -3478,8 +3541,10 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
|||||||
if not curr_list:
|
if not curr_list:
|
||||||
await ctx.send("Server allowlist is empty.")
|
await ctx.send("Server allowlist is empty.")
|
||||||
return
|
return
|
||||||
|
if len(curr_list) > 1:
|
||||||
msg = _("Whitelisted Users and roles:")
|
msg = _("Allowed users and/or roles:")
|
||||||
|
else:
|
||||||
|
msg = _("Allowed user or role:")
|
||||||
for obj in curr_list:
|
for obj in curr_list:
|
||||||
msg += "\n\t- {}".format(obj)
|
msg += "\n\t- {}".format(obj)
|
||||||
|
|
||||||
@@ -3593,8 +3658,10 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
|||||||
if not curr_list:
|
if not curr_list:
|
||||||
await ctx.send("Server blocklist is empty.")
|
await ctx.send("Server blocklist is empty.")
|
||||||
return
|
return
|
||||||
|
if len(curr_list) > 1:
|
||||||
msg = _("Blacklisted Users and Roles:")
|
msg = _("Blocked users and/or roles:")
|
||||||
|
else:
|
||||||
|
msg = _("Blocked user or role:")
|
||||||
for obj in curr_list:
|
for obj in curr_list:
|
||||||
msg += "\n\t- {}".format(obj)
|
msg += "\n\t- {}".format(obj)
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class Dev(commands.Cog):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self._last_result = None
|
self._last_result = None
|
||||||
self.sessions = {}
|
self.sessions = {}
|
||||||
|
self.env_extensions = {}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def async_compile(source, filename, mode):
|
def async_compile(source, filename, mode):
|
||||||
@@ -92,6 +93,29 @@ class Dev(commands.Cog):
|
|||||||
token = ctx.bot.http.token
|
token = ctx.bot.http.token
|
||||||
return re.sub(re.escape(token), "[EXPUNGED]", input_, re.I)
|
return re.sub(re.escape(token), "[EXPUNGED]", input_, re.I)
|
||||||
|
|
||||||
|
def get_environment(self, ctx: commands.Context) -> dict:
|
||||||
|
env = {
|
||||||
|
"bot": ctx.bot,
|
||||||
|
"ctx": ctx,
|
||||||
|
"channel": ctx.channel,
|
||||||
|
"author": ctx.author,
|
||||||
|
"guild": ctx.guild,
|
||||||
|
"message": ctx.message,
|
||||||
|
"asyncio": asyncio,
|
||||||
|
"aiohttp": aiohttp,
|
||||||
|
"discord": discord,
|
||||||
|
"commands": commands,
|
||||||
|
"_": self._last_result,
|
||||||
|
"__name__": "__main__",
|
||||||
|
}
|
||||||
|
for name, value in self.env_extensions.items():
|
||||||
|
try:
|
||||||
|
env[name] = value(ctx)
|
||||||
|
except Exception as e:
|
||||||
|
traceback.clear_frames(e.__traceback__)
|
||||||
|
env[name] = e
|
||||||
|
return env
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def debug(self, ctx, *, code):
|
async def debug(self, ctx, *, code):
|
||||||
@@ -115,21 +139,7 @@ class Dev(commands.Cog):
|
|||||||
commands - redbot.core.commands
|
commands - redbot.core.commands
|
||||||
_ - The result of the last dev command.
|
_ - The result of the last dev command.
|
||||||
"""
|
"""
|
||||||
env = {
|
env = self.get_environment(ctx)
|
||||||
"bot": ctx.bot,
|
|
||||||
"ctx": ctx,
|
|
||||||
"channel": ctx.channel,
|
|
||||||
"author": ctx.author,
|
|
||||||
"guild": ctx.guild,
|
|
||||||
"message": ctx.message,
|
|
||||||
"asyncio": asyncio,
|
|
||||||
"aiohttp": aiohttp,
|
|
||||||
"discord": discord,
|
|
||||||
"commands": commands,
|
|
||||||
"_": self._last_result,
|
|
||||||
"__name__": "__main__",
|
|
||||||
}
|
|
||||||
|
|
||||||
code = self.cleanup_code(code)
|
code = self.cleanup_code(code)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -169,21 +179,7 @@ class Dev(commands.Cog):
|
|||||||
commands - redbot.core.commands
|
commands - redbot.core.commands
|
||||||
_ - The result of the last dev command.
|
_ - The result of the last dev command.
|
||||||
"""
|
"""
|
||||||
env = {
|
env = self.get_environment(ctx)
|
||||||
"bot": ctx.bot,
|
|
||||||
"ctx": ctx,
|
|
||||||
"channel": ctx.channel,
|
|
||||||
"author": ctx.author,
|
|
||||||
"guild": ctx.guild,
|
|
||||||
"message": ctx.message,
|
|
||||||
"asyncio": asyncio,
|
|
||||||
"aiohttp": aiohttp,
|
|
||||||
"discord": discord,
|
|
||||||
"commands": commands,
|
|
||||||
"_": self._last_result,
|
|
||||||
"__name__": "__main__",
|
|
||||||
}
|
|
||||||
|
|
||||||
body = self.cleanup_code(body)
|
body = self.cleanup_code(body)
|
||||||
stdout = io.StringIO()
|
stdout = io.StringIO()
|
||||||
|
|
||||||
@@ -224,19 +220,6 @@ class Dev(commands.Cog):
|
|||||||
backtick. This includes codeblocks, and as such multiple lines can be
|
backtick. This includes codeblocks, and as such multiple lines can be
|
||||||
evaluated.
|
evaluated.
|
||||||
"""
|
"""
|
||||||
variables = {
|
|
||||||
"ctx": ctx,
|
|
||||||
"bot": ctx.bot,
|
|
||||||
"message": ctx.message,
|
|
||||||
"guild": ctx.guild,
|
|
||||||
"channel": ctx.channel,
|
|
||||||
"author": ctx.author,
|
|
||||||
"asyncio": asyncio,
|
|
||||||
"_": None,
|
|
||||||
"__builtins__": __builtins__,
|
|
||||||
"__name__": "__main__",
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.channel.id in self.sessions:
|
if ctx.channel.id in self.sessions:
|
||||||
if self.sessions[ctx.channel.id]:
|
if self.sessions[ctx.channel.id]:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
@@ -250,6 +233,9 @@ class Dev(commands.Cog):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
env = self.get_environment(ctx)
|
||||||
|
env["__builtins__"] = __builtins__
|
||||||
|
env["_"] = None
|
||||||
self.sessions[ctx.channel.id] = True
|
self.sessions[ctx.channel.id] = True
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
@@ -287,8 +273,7 @@ class Dev(commands.Cog):
|
|||||||
await ctx.send(self.get_syntax_error(e))
|
await ctx.send(self.get_syntax_error(e))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
variables["message"] = response
|
env["message"] = response
|
||||||
|
|
||||||
stdout = io.StringIO()
|
stdout = io.StringIO()
|
||||||
|
|
||||||
msg = ""
|
msg = ""
|
||||||
@@ -296,9 +281,9 @@ class Dev(commands.Cog):
|
|||||||
try:
|
try:
|
||||||
with redirect_stdout(stdout):
|
with redirect_stdout(stdout):
|
||||||
if executor is None:
|
if executor is None:
|
||||||
result = types.FunctionType(code, variables)()
|
result = types.FunctionType(code, env)()
|
||||||
else:
|
else:
|
||||||
result = executor(code, variables)
|
result = executor(code, env)
|
||||||
result = await self.maybe_await(result)
|
result = await self.maybe_await(result)
|
||||||
except:
|
except:
|
||||||
value = stdout.getvalue()
|
value = stdout.getvalue()
|
||||||
@@ -307,7 +292,7 @@ class Dev(commands.Cog):
|
|||||||
value = stdout.getvalue()
|
value = stdout.getvalue()
|
||||||
if result is not None:
|
if result is not None:
|
||||||
msg = "{}{}".format(value, result)
|
msg = "{}{}".format(value, result)
|
||||||
variables["_"] = result
|
env["_"] = result
|
||||||
elif value:
|
elif value:
|
||||||
msg = "{}".format(value)
|
msg = "{}".format(value)
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ async def is_allowed_by_hierarchy(
|
|||||||
if not await settings.guild(guild).respect_hierarchy():
|
if not await settings.guild(guild).respect_hierarchy():
|
||||||
return True
|
return True
|
||||||
is_special = mod == guild.owner or await bot.is_owner(mod)
|
is_special = mod == guild.owner or await bot.is_owner(mod)
|
||||||
return mod.top_role.position > user.top_role.position or is_special
|
return mod.top_role > user.top_role or is_special
|
||||||
|
|
||||||
|
|
||||||
async def is_mod_or_superior(
|
async def is_mod_or_superior(
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
|||||||
def same_context(
|
def same_context(
|
||||||
cls,
|
cls,
|
||||||
ctx: Optional[commands.Context] = None,
|
ctx: Optional[commands.Context] = None,
|
||||||
channel: Optional[discord.TextChannel] = None,
|
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||||
user: Optional[discord.abc.User] = None,
|
user: Optional[discord.abc.User] = None,
|
||||||
) -> "MessagePredicate":
|
) -> "MessagePredicate":
|
||||||
"""Match if the message fits the described context.
|
"""Match if the message fits the described context.
|
||||||
@@ -104,7 +104,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
|||||||
def cancelled(
|
def cancelled(
|
||||||
cls,
|
cls,
|
||||||
ctx: Optional[commands.Context] = None,
|
ctx: Optional[commands.Context] = None,
|
||||||
channel: Optional[discord.TextChannel] = None,
|
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||||
user: Optional[discord.abc.User] = None,
|
user: Optional[discord.abc.User] = None,
|
||||||
) -> "MessagePredicate":
|
) -> "MessagePredicate":
|
||||||
"""Match if the message is ``[p]cancel``.
|
"""Match if the message is ``[p]cancel``.
|
||||||
@@ -133,7 +133,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
|||||||
def yes_or_no(
|
def yes_or_no(
|
||||||
cls,
|
cls,
|
||||||
ctx: Optional[commands.Context] = None,
|
ctx: Optional[commands.Context] = None,
|
||||||
channel: Optional[discord.TextChannel] = None,
|
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||||
user: Optional[discord.abc.User] = None,
|
user: Optional[discord.abc.User] = None,
|
||||||
) -> "MessagePredicate":
|
) -> "MessagePredicate":
|
||||||
"""Match if the message is "yes"/"y" or "no"/"n".
|
"""Match if the message is "yes"/"y" or "no"/"n".
|
||||||
@@ -176,7 +176,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
|||||||
def valid_int(
|
def valid_int(
|
||||||
cls,
|
cls,
|
||||||
ctx: Optional[commands.Context] = None,
|
ctx: Optional[commands.Context] = None,
|
||||||
channel: Optional[discord.TextChannel] = None,
|
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||||
user: Optional[discord.abc.User] = None,
|
user: Optional[discord.abc.User] = None,
|
||||||
) -> "MessagePredicate":
|
) -> "MessagePredicate":
|
||||||
"""Match if the response is an integer.
|
"""Match if the response is an integer.
|
||||||
@@ -216,7 +216,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
|||||||
def valid_float(
|
def valid_float(
|
||||||
cls,
|
cls,
|
||||||
ctx: Optional[commands.Context] = None,
|
ctx: Optional[commands.Context] = None,
|
||||||
channel: Optional[discord.TextChannel] = None,
|
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||||
user: Optional[discord.abc.User] = None,
|
user: Optional[discord.abc.User] = None,
|
||||||
) -> "MessagePredicate":
|
) -> "MessagePredicate":
|
||||||
"""Match if the response is a float.
|
"""Match if the response is a float.
|
||||||
@@ -256,7 +256,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
|||||||
def positive(
|
def positive(
|
||||||
cls,
|
cls,
|
||||||
ctx: Optional[commands.Context] = None,
|
ctx: Optional[commands.Context] = None,
|
||||||
channel: Optional[discord.TextChannel] = None,
|
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||||
user: Optional[discord.abc.User] = None,
|
user: Optional[discord.abc.User] = None,
|
||||||
) -> "MessagePredicate":
|
) -> "MessagePredicate":
|
||||||
"""Match if the response is a positive number.
|
"""Match if the response is a positive number.
|
||||||
@@ -492,7 +492,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
|||||||
cls,
|
cls,
|
||||||
value: str,
|
value: str,
|
||||||
ctx: Optional[commands.Context] = None,
|
ctx: Optional[commands.Context] = None,
|
||||||
channel: Optional[discord.TextChannel] = None,
|
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||||
user: Optional[discord.abc.User] = None,
|
user: Optional[discord.abc.User] = None,
|
||||||
) -> "MessagePredicate":
|
) -> "MessagePredicate":
|
||||||
"""Match if the response is equal to the specified value.
|
"""Match if the response is equal to the specified value.
|
||||||
@@ -522,7 +522,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
|||||||
cls,
|
cls,
|
||||||
value: str,
|
value: str,
|
||||||
ctx: Optional[commands.Context] = None,
|
ctx: Optional[commands.Context] = None,
|
||||||
channel: Optional[discord.TextChannel] = None,
|
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||||
user: Optional[discord.abc.User] = None,
|
user: Optional[discord.abc.User] = None,
|
||||||
) -> "MessagePredicate":
|
) -> "MessagePredicate":
|
||||||
"""Match if the response *as lowercase* is equal to the specified value.
|
"""Match if the response *as lowercase* is equal to the specified value.
|
||||||
@@ -552,7 +552,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
|||||||
cls,
|
cls,
|
||||||
value: Union[int, float],
|
value: Union[int, float],
|
||||||
ctx: Optional[commands.Context] = None,
|
ctx: Optional[commands.Context] = None,
|
||||||
channel: Optional[discord.TextChannel] = None,
|
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||||
user: Optional[discord.abc.User] = None,
|
user: Optional[discord.abc.User] = None,
|
||||||
) -> "MessagePredicate":
|
) -> "MessagePredicate":
|
||||||
"""Match if the response is less than the specified value.
|
"""Match if the response is less than the specified value.
|
||||||
@@ -583,7 +583,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
|||||||
cls,
|
cls,
|
||||||
value: Union[int, float],
|
value: Union[int, float],
|
||||||
ctx: Optional[commands.Context] = None,
|
ctx: Optional[commands.Context] = None,
|
||||||
channel: Optional[discord.TextChannel] = None,
|
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||||
user: Optional[discord.abc.User] = None,
|
user: Optional[discord.abc.User] = None,
|
||||||
) -> "MessagePredicate":
|
) -> "MessagePredicate":
|
||||||
"""Match if the response is greater than the specified value.
|
"""Match if the response is greater than the specified value.
|
||||||
@@ -614,7 +614,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
|||||||
cls,
|
cls,
|
||||||
length: int,
|
length: int,
|
||||||
ctx: Optional[commands.Context] = None,
|
ctx: Optional[commands.Context] = None,
|
||||||
channel: Optional[discord.TextChannel] = None,
|
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||||
user: Optional[discord.abc.User] = None,
|
user: Optional[discord.abc.User] = None,
|
||||||
) -> "MessagePredicate":
|
) -> "MessagePredicate":
|
||||||
"""Match if the response's length is less than the specified length.
|
"""Match if the response's length is less than the specified length.
|
||||||
@@ -644,7 +644,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
|||||||
cls,
|
cls,
|
||||||
length: int,
|
length: int,
|
||||||
ctx: Optional[commands.Context] = None,
|
ctx: Optional[commands.Context] = None,
|
||||||
channel: Optional[discord.TextChannel] = None,
|
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||||
user: Optional[discord.abc.User] = None,
|
user: Optional[discord.abc.User] = None,
|
||||||
) -> "MessagePredicate":
|
) -> "MessagePredicate":
|
||||||
"""Match if the response's length is greater than the specified length.
|
"""Match if the response's length is greater than the specified length.
|
||||||
@@ -674,7 +674,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
|||||||
cls,
|
cls,
|
||||||
collection: Sequence[str],
|
collection: Sequence[str],
|
||||||
ctx: Optional[commands.Context] = None,
|
ctx: Optional[commands.Context] = None,
|
||||||
channel: Optional[discord.TextChannel] = None,
|
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||||
user: Optional[discord.abc.User] = None,
|
user: Optional[discord.abc.User] = None,
|
||||||
) -> "MessagePredicate":
|
) -> "MessagePredicate":
|
||||||
"""Match if the response is contained in the specified collection.
|
"""Match if the response is contained in the specified collection.
|
||||||
@@ -718,7 +718,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
|||||||
cls,
|
cls,
|
||||||
collection: Sequence[str],
|
collection: Sequence[str],
|
||||||
ctx: Optional[commands.Context] = None,
|
ctx: Optional[commands.Context] = None,
|
||||||
channel: Optional[discord.TextChannel] = None,
|
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||||
user: Optional[discord.abc.User] = None,
|
user: Optional[discord.abc.User] = None,
|
||||||
) -> "MessagePredicate":
|
) -> "MessagePredicate":
|
||||||
"""Same as :meth:`contained_in`, but the response is set to lowercase before matching.
|
"""Same as :meth:`contained_in`, but the response is set to lowercase before matching.
|
||||||
@@ -759,7 +759,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
|||||||
cls,
|
cls,
|
||||||
pattern: Union[Pattern[str], str],
|
pattern: Union[Pattern[str], str],
|
||||||
ctx: Optional[commands.Context] = None,
|
ctx: Optional[commands.Context] = None,
|
||||||
channel: Optional[discord.TextChannel] = None,
|
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||||
user: Optional[discord.abc.User] = None,
|
user: Optional[discord.abc.User] = None,
|
||||||
) -> "MessagePredicate":
|
) -> "MessagePredicate":
|
||||||
"""Match if the response matches the specified regex pattern.
|
"""Match if the response matches the specified regex pattern.
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class RotatingFileHandler(logging.handlers.RotatingFileHandler):
|
|||||||
self.baseStem = stem
|
self.baseStem = stem
|
||||||
self.directory = directory.resolve()
|
self.directory = directory.resolve()
|
||||||
# Scan for existing files in directory, append to last part of existing log
|
# Scan for existing files in directory, append to last part of existing log
|
||||||
log_part_re = re.compile(rf"{stem}-part(?P<partnum>\d+).log")
|
log_part_re = re.compile(rf"{stem}-part(?P<partnum>\d)\.log")
|
||||||
highest_part = 0
|
highest_part = 0
|
||||||
for path in directory.iterdir():
|
for path in directory.iterdir():
|
||||||
match = log_part_re.match(path.name)
|
match = log_part_re.match(path.name)
|
||||||
@@ -86,7 +86,7 @@ class RotatingFileHandler(logging.handlers.RotatingFileHandler):
|
|||||||
initial_path.replace(self.directory / f"{self.baseStem}-part1.log")
|
initial_path.replace(self.directory / f"{self.baseStem}-part1.log")
|
||||||
|
|
||||||
match = re.match(
|
match = re.match(
|
||||||
rf"{self.baseStem}(?:-part(?P<part>\d+)?)?.log", pathlib.Path(self.baseFilename).name
|
rf"{self.baseStem}(?:-part(?P<part>\d))?\.log", pathlib.Path(self.baseFilename).name
|
||||||
)
|
)
|
||||||
latest_part_num = int(match.groupdict(default="1").get("part", "1"))
|
latest_part_num = int(match.groupdict(default="1").get("part", "1"))
|
||||||
if self.backupCount < 1:
|
if self.backupCount < 1:
|
||||||
@@ -95,7 +95,7 @@ class RotatingFileHandler(logging.handlers.RotatingFileHandler):
|
|||||||
elif latest_part_num > self.backupCount:
|
elif latest_part_num > self.backupCount:
|
||||||
# Rotate files down one
|
# Rotate files down one
|
||||||
# red-part2.log becomes red-part1.log etc, a new log is added at the end.
|
# red-part2.log becomes red-part1.log etc, a new log is added at the end.
|
||||||
for i in range(1, self.backupCount):
|
for i in range(1, self.backupCount + 1):
|
||||||
next_log = self.directory / f"{self.baseStem}-part{i + 1}.log"
|
next_log = self.directory / f"{self.baseStem}-part{i + 1}.log"
|
||||||
if next_log.exists():
|
if next_log.exists():
|
||||||
prev_log = self.directory / f"{self.baseStem}-part{i}.log"
|
prev_log = self.directory / f"{self.baseStem}-part{i}.log"
|
||||||
|
|||||||
Reference in New Issue
Block a user