Merge branch 'V3/release/3.0.0' into V3/develop

# Conflicts:
#	redbot/cogs/customcom/customcom.py
#	redbot/core/errors.py
This commit is contained in:
Toby Harradine 2018-10-16 09:42:38 +11:00
commit 8b4e12da81
11 changed files with 317 additions and 195 deletions

View File

@ -1,6 +1,6 @@
import re import re
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Union, List, Callable from typing import Union, List, Callable, Set
import discord import discord
@ -323,15 +323,35 @@ class Cleanup(commands.Cog):
if "" in prefixes: if "" in prefixes:
prefixes.remove("") prefixes.remove("")
cc_cog = self.bot.get_cog("CustomCommands")
if cc_cog is not None:
command_names: Set[str] = await cc_cog.get_command_names(ctx.guild)
is_cc = lambda name: name in command_names
else:
is_cc = lambda name: False
alias_cog = self.bot.get_cog("Alias")
if alias_cog is not None:
alias_names: Set[str] = (
set((a.name for a in await alias_cog.unloaded_global_aliases()))
| set(a.name for a in await alias_cog.unloaded_aliases(ctx.guild))
)
is_alias = lambda name: name in alias_names
else:
is_alias = lambda name: False
bot_id = self.bot.user.id
def check(m): def check(m):
if m.author.id == self.bot.user.id: if m.author.id == bot_id:
return True return True
elif m == ctx.message: elif m == ctx.message:
return True return True
p = discord.utils.find(m.content.startswith, prefixes) p = discord.utils.find(m.content.startswith, prefixes)
if p and len(p) > 0: if p and len(p) > 0:
cmd_name = m.content[len(p) :].split(" ")[0] cmd_name = m.content[len(p) :].split(" ")[0]
return bool(self.bot.get_command(cmd_name)) return (
bool(self.bot.get_command(cmd_name)) or is_alias(cmd_name) or is_cc(cmd_name)
)
return False return False
to_delete = await self.get_messages_for_deletion( to_delete = await self.get_messages_for_deletion(

View File

@ -3,13 +3,14 @@ import random
from datetime import datetime, timedelta from datetime import datetime, timedelta
from inspect import Parameter from inspect import Parameter
from collections import OrderedDict from collections import OrderedDict
from typing import Mapping, Tuple, Dict from typing import Mapping, Tuple, Dict, Set
import discord import discord
from redbot.core import Config, checks, commands from redbot.core import Config, checks, commands
from redbot.core.utils.chat_formatting import box, pagify, escape
from redbot.core.i18n import Translator, cog_i18n from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils import menus
from redbot.core.utils.chat_formatting import box, pagify, escape
from redbot.core.utils.predicates import MessagePredicate from redbot.core.utils.predicates import MessagePredicate
_ = Translator("CustomCommands", __file__) _ = Translator("CustomCommands", __file__)
@ -121,7 +122,7 @@ class CommandObj:
*, *,
response=None, response=None,
cooldowns: Mapping[str, int] = None, cooldowns: Mapping[str, int] = None,
ask_for: bool = True ask_for: bool = True,
): ):
"""Edit an already existing custom command""" """Edit an already existing custom command"""
ccinfo = await self.db(ctx.guild).commands.get_raw(command, default=None) ccinfo = await self.db(ctx.guild).commands.get_raw(command, default=None)
@ -330,12 +331,16 @@ class CustomCommands(commands.Cog):
await ctx.send(e.args[0]) await ctx.send(e.args[0])
@customcom.command(name="list") @customcom.command(name="list")
async def cc_list(self, ctx): @checks.bot_has_permissions(add_reactions=True)
"""List all available custom commands.""" async def cc_list(self, ctx: commands.Context):
"""List all available custom commands.
response = await CommandObj.get_commands(self.config.guild(ctx.guild)) The list displays a preview of each command's response, with
markdown escaped and newlines replaced with spaces.
"""
cc_dict = await CommandObj.get_commands(self.config.guild(ctx.guild))
if not response: if not cc_dict:
await ctx.send( await ctx.send(
_( _(
"There are no custom commands in this server." "There are no custom commands in this server."
@ -345,8 +350,7 @@ class CustomCommands(commands.Cog):
return return
results = [] results = []
for command, body in sorted(cc_dict.items(), key=lambda t: t[0]):
for command, body in response.items():
responses = body["response"] responses = body["response"]
if isinstance(responses, list): if isinstance(responses, list):
result = ", ".join(responses) result = ", ".join(responses)
@ -354,15 +358,33 @@ class CustomCommands(commands.Cog):
result = responses result = responses
else: else:
continue continue
results.append("{command:<15} : {result}".format(command=command, result=result)) # Replace newlines with spaces
# Cut preview to 52 characters max
if len(result) > 52:
result = result[:49] + "..."
# Replace newlines with spaces
result = result.replace("\n", " ")
# Escape markdown and mass mentions
result = escape(result, formatting=True, mass_mentions=True)
results.append((f"{ctx.clean_prefix}{command}", result))
_commands = "\n".join(results) if await ctx.embed_requested():
content = "\n".join(map("**{0[0]}** {0[1]}".format, results))
if len(_commands) < 1500: pages = list(pagify(content, page_length=1024))
await ctx.send(box(_commands)) embed_pages = []
for idx, page in enumerate(pages, start=1):
embed = discord.Embed(
title=_("Custom Command List"),
description=page,
colour=await ctx.embed_colour(),
)
embed.set_footer(text=_("Page {num}/{total}").format(num=idx, total=len(pages)))
embed_pages.append(embed)
await menus.menu(ctx, embed_pages, menus.DEFAULT_CONTROLS)
else: else:
for page in pagify(_commands, delims=[" ", "\n"]): content = "\n".join(map("{0[0]:<12} : {0[1]}".format, results))
await ctx.author.send(box(page)) pages = list(map(box, pagify(content, page_length=2000, shorten_by=10)))
await menus.menu(ctx, pages, menus.DEFAULT_CONTROLS)
@customcom.command(name="show") @customcom.command(name="show")
async def cc_show(self, ctx, command_name: str): async def cc_show(self, ctx, command_name: str):
@ -606,3 +628,14 @@ class CustomCommands(commands.Cog):
else: else:
return raw_result return raw_result
return str(getattr(first, second, raw_result)) return str(getattr(first, second, raw_result))
async def get_command_names(self, guild: discord.Guild) -> Set[str]:
"""Get all custom command names in a guild.
Returns
--------
Set[str]
A set of all custom command names.
"""
return set(await CommandObj.get_commands(self.config.guild(guild)))

View File

@ -1,7 +1,10 @@
import discord import discord
from redbot.core import commands from redbot.core import commands
from redbot.core.i18n import Translator
from .installable import Installable from .installable import Installable
_ = Translator("Koala", __file__)
class InstalledCog(Installable): class InstalledCog(Installable):
@classmethod @classmethod

View File

@ -325,13 +325,12 @@ class Downloader(commands.Cog):
You may only uninstall cogs which were previously installed You may only uninstall cogs which were previously installed
by Downloader. by Downloader.
""" """
# noinspection PyUnresolvedReferences,PyProtectedMember
real_name = cog.name real_name = cog.name
poss_installed_path = (await self.cog_install_path()) / real_name poss_installed_path = (await self.cog_install_path()) / real_name
if poss_installed_path.exists(): if poss_installed_path.exists():
ctx.bot.unload_extension(real_name)
await self._delete_cog(poss_installed_path) await self._delete_cog(poss_installed_path)
# noinspection PyTypeChecker
await self._remove_from_installed(cog) await self._remove_from_installed(cog)
await ctx.send( await ctx.send(
_("Cog `{cog_name}` was successfully uninstalled.").format(cog_name=real_name) _("Cog `{cog_name}` was successfully uninstalled.").format(cog_name=real_name)
@ -344,7 +343,7 @@ class Downloader(commands.Cog):
" files manually if it is still usable." " files manually if it is still usable."
" Also make sure you've unloaded the cog" " Also make sure you've unloaded the cog"
" with `{prefix}unload {cog_name}`." " with `{prefix}unload {cog_name}`."
).format(cog_name=real_name) ).format(prefix=ctx.prefix, cog_name=real_name)
) )
@cog.command(name="update") @cog.command(name="update")
@ -372,13 +371,18 @@ class Downloader(commands.Cog):
await self._reinstall_libraries(installed_and_updated) await self._reinstall_libraries(installed_and_updated)
message = _("Cog update completed successfully.") message = _("Cog update completed successfully.")
cognames = [c.name for c in installed_and_updated] cognames = {c.name for c in installed_and_updated}
message += _("\nUpdated: ") + humanize_list(tuple(map(inline, cognames))) message += _("\nUpdated: ") + humanize_list(tuple(map(inline, cognames)))
else: else:
await ctx.send(_("All installed cogs are already up to date.")) await ctx.send(_("All installed cogs are already up to date."))
return return
await ctx.send(message) await ctx.send(message)
cognames &= set(ctx.bot.extensions.keys()) # only reload loaded cogs
if not cognames:
return await ctx.send(
_("None of the updated cogs were previously loaded. Update complete.")
)
message = _("Would you like to reload the updated cogs?") message = _("Would you like to reload the updated cogs?")
can_react = ctx.channel.permissions_for(ctx.me).add_reactions can_react = ctx.channel.permissions_for(ctx.me).add_reactions
if not can_react: if not can_react:
@ -402,7 +406,6 @@ class Downloader(commands.Cog):
if can_react: if can_react:
with contextlib.suppress(discord.Forbidden): with contextlib.suppress(discord.Forbidden):
await query.clear_reactions() await query.clear_reactions()
await ctx.invoke(ctx.bot.get_cog("Core").reload, *cognames) await ctx.invoke(ctx.bot.get_cog("Core").reload, *cognames)
else: else:
if can_react: if can_react:

View File

@ -8,7 +8,7 @@ from typing import cast, Iterable
import discord import discord
from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin
from redbot.core import Config, bank, commands from redbot.core import Config, bank, commands, errors
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
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
@ -171,7 +171,7 @@ class Economy(commands.Cog):
try: try:
await bank.transfer_credits(from_, to, amount) await bank.transfer_credits(from_, to, amount)
except ValueError as e: except (ValueError, errors.BalanceTooHigh) as e:
return await ctx.send(str(e)) return await ctx.send(str(e))
await ctx.send( await ctx.send(
@ -195,36 +195,35 @@ class Economy(commands.Cog):
author = ctx.author author = ctx.author
currency = await bank.get_currency_name(ctx.guild) currency = await bank.get_currency_name(ctx.guild)
if creds.operation == "deposit": try:
await bank.deposit_credits(to, creds.sum) if creds.operation == "deposit":
await ctx.send( await bank.deposit_credits(to, creds.sum)
_("{author} added {num} {currency} to {user}'s account.").format( msg = _("{author} added {num} {currency} to {user}'s account.").format(
author=author.display_name, author=author.display_name,
num=creds.sum, num=creds.sum,
currency=currency, currency=currency,
user=to.display_name, user=to.display_name,
) )
) elif creds.operation == "withdraw":
elif creds.operation == "withdraw": await bank.withdraw_credits(to, creds.sum)
await bank.withdraw_credits(to, creds.sum) msg = _("{author} removed {num} {currency} from {user}'s account.").format(
await ctx.send(
_("{author} removed {num} {currency} from {user}'s account.").format(
author=author.display_name, author=author.display_name,
num=creds.sum, num=creds.sum,
currency=currency, currency=currency,
user=to.display_name, user=to.display_name,
) )
) else:
await bank.set_balance(to, creds.sum)
msg = _("{author} set {user}'s account balance to {num} {currency}.").format(
author=author.display_name,
num=creds.sum,
currency=currency,
user=to.display_name,
)
except (ValueError, errors.BalanceTooHigh) as e:
await ctx.send(str(e))
else: else:
await bank.set_balance(to, creds.sum) await ctx.send(msg)
await ctx.send(
_("{author} set {user}'s account balance to {num} {currency}.").format(
author=author.display_name,
num=creds.sum,
currency=currency,
user=to.display_name,
)
)
@_bank.command() @_bank.command()
@check_global_setting_guildowner() @check_global_setting_guildowner()
@ -260,7 +259,18 @@ class Economy(commands.Cog):
if await bank.is_global(): # Role payouts will not be used if await bank.is_global(): # Role payouts will not be used
next_payday = await self.config.user(author).next_payday() next_payday = await self.config.user(author).next_payday()
if cur_time >= next_payday: if cur_time >= next_payday:
await bank.deposit_credits(author, await self.config.PAYDAY_CREDITS()) try:
await bank.deposit_credits(author, await self.config.PAYDAY_CREDITS())
except errors.BalanceTooHigh as exc:
await bank.set_balance(author, exc.max_balance)
await ctx.send(
_(
"You've reached the maximum amount of {currency}! (**{balance:,}**) "
"Please spend some more \N{GRIMACING FACE}\n\n"
"You currently have {new_balance} {currency}."
).format(currency=credits_name, new_balance=exc.max_balance)
)
return
next_payday = cur_time + await self.config.PAYDAY_TIME() next_payday = cur_time + await self.config.PAYDAY_TIME()
await self.config.user(author).next_payday.set(next_payday) await self.config.user(author).next_payday.set(next_payday)
@ -297,14 +307,25 @@ class Economy(commands.Cog):
).PAYDAY_CREDITS() # Nice variable name ).PAYDAY_CREDITS() # Nice variable name
if role_credits > credit_amount: if role_credits > credit_amount:
credit_amount = role_credits credit_amount = role_credits
await bank.deposit_credits(author, credit_amount) try:
await bank.deposit_credits(author, credit_amount)
except errors.BalanceTooHigh as exc:
await bank.set_balance(author, exc.max_balance)
await ctx.send(
_(
"You've reached the maximum amount of {currency}! "
"Please spend some more \N{GRIMACING FACE}\n\n"
"You currently have {new_balance} {currency}."
).format(currency=credits_name, new_balance=exc.max_balance)
)
return
next_payday = cur_time + await self.config.guild(guild).PAYDAY_TIME() next_payday = cur_time + await self.config.guild(guild).PAYDAY_TIME()
await self.config.member(author).next_payday.set(next_payday) await self.config.member(author).next_payday.set(next_payday)
pos = await bank.get_leaderboard_position(author) pos = await bank.get_leaderboard_position(author)
await ctx.send( await ctx.send(
_( _(
"{author.mention} Here, take some {currency}. " "{author.mention} Here, take some {currency}. "
"Enjoy! (+{amount} {new_balance}!)\n\n" "Enjoy! (+{amount} {currency}!)\n\n"
"You currently have {new_balance} {currency}.\n\n" "You currently have {new_balance} {currency}.\n\n"
"You are currently #{pos} on the global leaderboard!" "You are currently #{pos} on the global leaderboard!"
).format( ).format(
@ -444,7 +465,21 @@ class Economy(commands.Cog):
then = await bank.get_balance(author) then = await bank.get_balance(author)
pay = payout["payout"](bid) pay = payout["payout"](bid)
now = then - bid + pay now = then - bid + pay
await bank.set_balance(author, now) try:
await bank.set_balance(author, now)
except errors.BalanceTooHigh as exc:
await bank.set_balance(author, exc.max_balance)
await channel.send(
_(
"You've reached the maximum amount of {currency}! "
"Please spend some more \N{GRIMACING FACE}\n{old_balance} -> {new_balance}!"
).format(
currency=await bank.get_currency_name(getattr(channel, "guild", None)),
old_balance=then,
new_balance=exc.max_balance,
)
)
return
phrase = T_(payout["phrase"]) phrase = T_(payout["phrase"])
else: else:
then = await bank.get_balance(author) then = await bank.get_balance(author)
@ -561,10 +596,10 @@ class Economy(commands.Cog):
async def paydayamount(self, ctx: commands.Context, creds: int): async def paydayamount(self, ctx: commands.Context, creds: int):
"""Set the amount earned each payday.""" """Set the amount earned each payday."""
guild = ctx.guild guild = ctx.guild
credits_name = await bank.get_currency_name(guild) if creds <= 0 or creds > bank.MAX_BALANCE:
if creds <= 0:
await ctx.send(_("Har har so funny.")) await ctx.send(_("Har har so funny."))
return return
credits_name = await bank.get_currency_name(guild)
if await bank.is_global(): if await bank.is_global():
await self.config.PAYDAY_CREDITS.set(creds) await self.config.PAYDAY_CREDITS.set(creds)
else: else:
@ -579,6 +614,9 @@ class Economy(commands.Cog):
async def rolepaydayamount(self, ctx: commands.Context, role: discord.Role, creds: int): async def rolepaydayamount(self, ctx: commands.Context, role: discord.Role, creds: int):
"""Set the amount earned each payday for a role.""" """Set the amount earned each payday for a role."""
guild = ctx.guild guild = ctx.guild
if creds <= 0 or creds > bank.MAX_BALANCE:
await ctx.send(_("Har har so funny."))
return
credits_name = await bank.get_currency_name(guild) credits_name = await bank.get_currency_name(guild)
if await bank.is_global(): if await bank.is_global():
await ctx.send(_("The bank must be per-server for per-role paydays to work.")) await ctx.send(_("The bank must be per-server for per-role paydays to work."))

View File

@ -825,7 +825,7 @@ class Mod(commands.Cog):
@admin_or_voice_permissions(mute_members=True, deafen_members=True) @admin_or_voice_permissions(mute_members=True, deafen_members=True)
@bot_has_voice_permissions(mute_members=True, deafen_members=True) @bot_has_voice_permissions(mute_members=True, deafen_members=True)
async def voiceunban(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): async def voiceunban(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
"""Unban a the user from speaking and listening in the server's voice channels.""" """Unban a user from speaking and listening in the server's voice channels."""
user_voice_state = user.voice user_voice_state = user.voice
if user_voice_state is None: if user_voice_state is None:
await ctx.send(_("No voice state for that user!")) await ctx.send(_("No voice state for that user!"))
@ -893,34 +893,33 @@ class Mod(commands.Cog):
author = ctx.author author = ctx.author
if user_voice_state: if user_voice_state:
channel = user_voice_state.channel channel = user_voice_state.channel
if channel and channel.permissions_for(user).speak: if channel:
overwrites = channel.overwrites_for(user) audit_reason = get_audit_reason(author, reason)
overwrites.speak = False
audit_reason = get_audit_reason(ctx.author, reason) success, issue = await self.mute_user(guild, channel, author, user, audit_reason)
await channel.set_permissions(user, overwrite=overwrites, reason=audit_reason)
await ctx.send( if success:
_("Muted {user} in channel {channel.name}").format(user, channel=channel) await ctx.send(
) _("Muted {user} in channel {channel.name}").format(
try: user=user, channel=channel
await modlog.create_case( )
self.bot,
guild,
ctx.message.created_at,
"boicemute",
user,
author,
reason,
until=None,
channel=channel,
) )
except RuntimeError as e: try:
await ctx.send(e) await modlog.create_case(
return self.bot,
elif channel.permissions_for(user).speak is False: guild,
await ctx.send( ctx.message.created_at,
_("That user is already muted in {channel}!").format(channel=channel.name) "vmute",
) user,
return author,
reason,
until=None,
channel=channel,
)
except RuntimeError as e:
await ctx.send(e)
else:
await channel.send(issue)
else: else:
await ctx.send(_("That user is not in a voice channel right now!")) await ctx.send(_("That user is not in a voice channel right now!"))
else: else:
@ -938,13 +937,7 @@ class Mod(commands.Cog):
author = ctx.message.author author = ctx.message.author
channel = ctx.message.channel channel = ctx.message.channel
guild = ctx.guild guild = ctx.guild
audit_reason = get_audit_reason(author, reason)
if reason is None:
audit_reason = "Channel mute requested by {a} (ID {a.id})".format(a=author)
else:
audit_reason = "Channel mute requested by {a} (ID {a.id}). Reason: {r}".format(
a=author, r=reason
)
success, issue = await self.mute_user(guild, channel, author, user, audit_reason) success, issue = await self.mute_user(guild, channel, author, user, audit_reason)
@ -975,26 +968,12 @@ class Mod(commands.Cog):
"""Mutes user in the server""" """Mutes user in the server"""
author = ctx.message.author author = ctx.message.author
guild = ctx.guild guild = ctx.guild
if reason is None: audit_reason = get_audit_reason(author, reason)
audit_reason = "server mute requested by {author} (ID {author.id})".format(
author=author
)
else:
audit_reason = (
"server mute requested by {author} (ID {author.id}). Reason: {reason}"
).format(author=author, reason=reason)
mute_success = [] mute_success = []
for channel in guild.channels: for channel in guild.channels:
if not isinstance(channel, discord.TextChannel): success, issue = await self.mute_user(guild, channel, author, user, audit_reason)
if channel.permissions_for(user).speak: mute_success.append((success, issue))
overwrites = channel.overwrites_for(user)
overwrites.speak = False
audit_reason = get_audit_reason(ctx.author, reason)
await channel.set_permissions(user, overwrite=overwrites, reason=audit_reason)
else:
success, issue = await self.mute_user(guild, channel, author, user, audit_reason)
mute_success.append((success, issue))
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
await ctx.send(_("User has been muted in this server.")) await ctx.send(_("User has been muted in this server."))
try: try:
@ -1015,7 +994,7 @@ class Mod(commands.Cog):
async def mute_user( async def mute_user(
self, self,
guild: discord.Guild, guild: discord.Guild,
channel: discord.TextChannel, channel: discord.abc.GuildChannel,
author: discord.Member, author: discord.Member,
user: discord.Member, user: discord.Member,
reason: str, reason: str,
@ -1023,25 +1002,32 @@ class Mod(commands.Cog):
"""Mutes the specified user in the specified channel""" """Mutes the specified user in the specified channel"""
overwrites = channel.overwrites_for(user) overwrites = channel.overwrites_for(user)
permissions = channel.permissions_for(user) permissions = channel.permissions_for(user)
perms_cache = await self.settings.member(user).perms_cache()
if overwrites.send_messages is False or permissions.send_messages is False: if permissions.administrator:
return False, T_(mute_unmute_issues["is_admin"])
new_overs = {}
if not isinstance(channel, discord.TextChannel):
new_overs.update(speak=False)
if not isinstance(channel, discord.VoiceChannel):
new_overs.update(send_messages=False, add_reactions=False)
if all(getattr(permissions, p) is False for p in new_overs.keys()):
return False, T_(mute_unmute_issues["already_muted"]) return False, T_(mute_unmute_issues["already_muted"])
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user): elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
return False, T_(mute_unmute_issues["hierarchy_problem"]) return False, T_(mute_unmute_issues["hierarchy_problem"])
perms_cache[str(channel.id)] = { old_overs = {k: getattr(overwrites, k) for k in new_overs}
"send_messages": overwrites.send_messages, overwrites.update(**new_overs)
"add_reactions": overwrites.add_reactions,
}
overwrites.update(send_messages=False, add_reactions=False)
try: try:
await channel.set_permissions(user, overwrite=overwrites, reason=reason) await channel.set_permissions(user, overwrite=overwrites, reason=reason)
except discord.Forbidden: except discord.Forbidden:
return False, T_(mute_unmute_issues["permissions_issue"]) return False, T_(mute_unmute_issues["permissions_issue"])
else: else:
await self.settings.member(user).perms_cache.set(perms_cache) await self.settings.member(user).set_raw(
"perms_cache", str(channel.id), value=old_overs
)
return True, None return True, None
@commands.group() @commands.group()
@ -1061,37 +1047,39 @@ class Mod(commands.Cog):
): ):
"""Unmute a user in their current voice channel.""" """Unmute a user in their current voice channel."""
user_voice_state = user.voice user_voice_state = user.voice
guild = ctx.guild
author = ctx.author
if user_voice_state: if user_voice_state:
channel = user_voice_state.channel channel = user_voice_state.channel
if channel and channel.permissions_for(user).speak is False: if channel:
overwrites = channel.overwrites_for(user) audit_reason = get_audit_reason(author, reason)
overwrites.speak = None
audit_reason = get_audit_reason(ctx.author, reason) success, message = await self.unmute_user(
await channel.set_permissions(user, overwrite=overwrites, reason=audit_reason) guild, channel, author, user, audit_reason
author = ctx.author
guild = ctx.guild
await ctx.send(
_("Unmuted {}#{} in channel {}").format(
user.name, user.discriminator, channel.name
)
) )
try:
await modlog.create_case( if success:
self.bot, await ctx.send(
guild, _("Unmuted {user} in channel {channel.name}").format(
ctx.message.created_at, user=user, channel=channel
"voiceunmute", )
user,
author,
reason,
until=None,
channel=channel,
) )
except RuntimeError as e: try:
await ctx.send(e) await modlog.create_case(
elif channel.permissions_for(user).speak: self.bot,
await ctx.send(_("That user is already unmuted in {}!").format(channel.name)) guild,
return ctx.message.created_at,
"vunmute",
user,
author,
reason,
until=None,
channel=channel,
)
except RuntimeError as e:
await ctx.send(e)
else:
await ctx.send(_("Unmute failed. Reason: {}").format(message))
else: else:
await ctx.send(_("That user is not in a voice channel right now!")) await ctx.send(_("That user is not in a voice channel right now!"))
else: else:
@ -1109,8 +1097,9 @@ class Mod(commands.Cog):
channel = ctx.channel channel = ctx.channel
author = ctx.author author = ctx.author
guild = ctx.guild guild = ctx.guild
audit_reason = get_audit_reason(author, reason)
success, message = await self.unmute_user(guild, channel, author, user) success, message = await self.unmute_user(guild, channel, author, user, audit_reason)
if success: if success:
await ctx.send(_("User unmuted in this channel.")) await ctx.send(_("User unmuted in this channel."))
@ -1141,16 +1130,11 @@ class Mod(commands.Cog):
"""Unmute a user in this server.""" """Unmute a user in this server."""
guild = ctx.guild guild = ctx.guild
author = ctx.author author = ctx.author
audit_reason = get_audit_reason(author, reason)
unmute_success = [] unmute_success = []
for channel in guild.channels: for channel in guild.channels:
if not isinstance(channel, discord.TextChannel): success, message = await self.unmute_user(guild, channel, author, user, audit_reason)
if channel.permissions_for(user).speak is False:
overwrites = channel.overwrites_for(user)
overwrites.speak = None
audit_reason = get_audit_reason(author, reason)
await channel.set_permissions(user, overwrite=overwrites, reason=audit_reason)
success, message = await self.unmute_user(guild, channel, author, user)
unmute_success.append((success, message)) unmute_success.append((success, message))
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
await ctx.send(_("User has been unmuted in this server.")) await ctx.send(_("User has been unmuted in this server."))
@ -1171,45 +1155,37 @@ class Mod(commands.Cog):
async def unmute_user( async def unmute_user(
self, self,
guild: discord.Guild, guild: discord.Guild,
channel: discord.TextChannel, channel: discord.abc.GuildChannel,
author: discord.Member, author: discord.Member,
user: discord.Member, user: discord.Member,
reason: str,
) -> (bool, str): ) -> (bool, str):
overwrites = channel.overwrites_for(user) overwrites = channel.overwrites_for(user)
permissions = channel.permissions_for(user)
perms_cache = await self.settings.member(user).perms_cache() perms_cache = await self.settings.member(user).perms_cache()
if overwrites.send_messages or permissions.send_messages: if channel.id in perms_cache:
old_values = perms_cache[channel.id]
else:
old_values = {"send_messages": None, "add_reactions": None, "speak": None}
if all(getattr(overwrites, k) == v for k, v in old_values.items()):
return False, T_(mute_unmute_issues["already_unmuted"]) return False, T_(mute_unmute_issues["already_unmuted"])
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user): elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
return False, T_(mute_unmute_issues["hierarchy_problem"]) return False, T_(mute_unmute_issues["hierarchy_problem"])
if channel.id in perms_cache: overwrites.update(**old_values)
old_values = perms_cache[channel.id]
else:
old_values = {"send_messages": None, "add_reactions": None}
overwrites.update(
send_messages=old_values["send_messages"], add_reactions=old_values["add_reactions"]
)
is_empty = self.are_overwrites_empty(overwrites)
try: try:
if not is_empty: if overwrites.is_empty():
await channel.set_permissions(user, overwrite=overwrites)
else:
await channel.set_permissions( await channel.set_permissions(
user, overwrite=cast(discord.PermissionOverwrite, None) user, overwrite=cast(discord.PermissionOverwrite, None), reason=reason
) )
else:
await channel.set_permissions(user, overwrite=overwrites, reason=reason)
except discord.Forbidden: except discord.Forbidden:
return False, T_(mute_unmute_issues["permissions_issue"]) return False, T_(mute_unmute_issues["permissions_issue"])
else: else:
try: await self.settings.member(user).clear_raw("perms_cache", str(channel.id))
del perms_cache[channel.id]
except KeyError:
pass
else:
await self.settings.member(user).perms_cache.set(perms_cache)
return True, None return True, None
@commands.group() @commands.group()
@ -1695,20 +1671,15 @@ class Mod(commands.Cog):
while len(nick_list) > 20: while len(nick_list) > 20:
nick_list.pop(0) nick_list.pop(0)
@staticmethod
def are_overwrites_empty(overwrites):
"""There is currently no cleaner way to check if a
PermissionOverwrite object is empty"""
return [p for p in iter(overwrites)] == [p for p in iter(discord.PermissionOverwrite())]
_ = lambda s: s _ = lambda s: s
mute_unmute_issues = { mute_unmute_issues = {
"already_muted": _("That user can't send messages in this channel."), "already_muted": _("That user can't send messages in this channel."),
"already_unmuted": _("That user isn't muted in this channel!"), "already_unmuted": _("That user isn't muted in this channel."),
"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."
), ),
"is_admin": _("That user cannot be muted, as they have the Administrator permission."),
"permissions_issue": _( "permissions_issue": _(
"Failed to mute user. I need the manage roles " "Failed to mute user. I need the manage roles "
"permission and the user I'm muting must be " "permission and the user I'm muting must be "

View File

@ -626,8 +626,13 @@ class Streams(commands.Cog):
raw_stream["_messages_cache"] = [] raw_stream["_messages_cache"] = []
for raw_msg in raw_msg_cache: for raw_msg in raw_msg_cache:
chn = self.bot.get_channel(raw_msg["channel"]) chn = self.bot.get_channel(raw_msg["channel"])
msg = await chn.get_message(raw_msg["message"]) if chn is not None:
raw_stream["_messages_cache"].append(msg) try:
msg = await chn.get_message(raw_msg["message"])
except discord.HTTPException:
pass
else:
raw_stream["_messages_cache"].append(msg)
token = await self.db.tokens.get_raw(_class.__name__, default=None) token = await self.db.tokens.get_raw(_class.__name__, default=None)
if token is not None: if token is not None:
raw_stream["token"] = token raw_stream["token"] = token
@ -646,8 +651,13 @@ class Streams(commands.Cog):
raw_community["_messages_cache"] = [] raw_community["_messages_cache"] = []
for raw_msg in raw_msg_cache: for raw_msg in raw_msg_cache:
chn = self.bot.get_channel(raw_msg["channel"]) chn = self.bot.get_channel(raw_msg["channel"])
msg = await chn.get_message(raw_msg["message"]) if chn is not None:
raw_community["_messages_cache"].append(msg) try:
msg = await chn.get_message(raw_msg["message"])
except discord.HTTPException:
pass
else:
raw_community["_messages_cache"].append(msg)
token = await self.db.tokens.get_raw(_class.__name__, default=None) token = await self.db.tokens.get_raw(_class.__name__, default=None)
communities.append(_class(token=token, **raw_community)) communities.append(_class(token=token, **raw_community))

View File

@ -148,5 +148,5 @@ class VersionInfo:
) )
__version__ = "3.0.0rc1.post1" __version__ = "3.0.0rc2"
version_info = VersionInfo.from_str(__version__) version_info = VersionInfo.from_str(__version__)

View File

@ -4,9 +4,10 @@ from typing import Union, List, Optional
import discord import discord
from redbot.core import Config from . import Config, errors
__all__ = [ __all__ = [
"MAX_BALANCE",
"Account", "Account",
"get_balance", "get_balance",
"set_balance", "set_balance",
@ -26,6 +27,8 @@ __all__ = [
"set_default_balance", "set_default_balance",
] ]
MAX_BALANCE = 2 ** 63 - 1
_DEFAULT_GLOBAL = { _DEFAULT_GLOBAL = {
"is_global": False, "is_global": False,
"bank_name": "Twentysix bank", "bank_name": "Twentysix bank",
@ -170,10 +173,22 @@ async def set_balance(member: discord.Member, amount: int) -> int:
------ ------
ValueError ValueError
If attempting to set the balance to a negative number. If attempting to set the balance to a negative number.
BalanceTooHigh
If attempting to set the balance to a value greater than
``bank.MAX_BALANCE``
""" """
if amount < 0: if amount < 0:
raise ValueError("Not allowed to have negative balance.") raise ValueError("Not allowed to have negative balance.")
if amount > MAX_BALANCE:
currency = (
await get_currency_name()
if await is_global()
else await get_currency_name(member.guild)
)
raise errors.BalanceTooHigh(
user=member.display_name, max_balance=MAX_BALANCE, currency_name=currency
)
if await is_global(): if await is_global():
group = _conf.user(member) group = _conf.user(member)
else: else:

View File

@ -1,4 +1,5 @@
import asyncio import asyncio
import inspect
import os import os
import logging import logging
from collections import Counter from collections import Counter
@ -236,20 +237,13 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
if cog is None: if cog is None:
return return
for when in ("before", "after"): for cls in inspect.getmro(cog.__class__):
try: try:
hook = getattr(cog, f"_{cog.__class__.__name__}__red_permissions_{when}") hook = getattr(cog, f"_{cls.__name__}__permissions_hook")
except AttributeError: except AttributeError:
pass pass
else: else:
self.remove_permissions_hook(hook, when) self.remove_permissions_hook(hook)
try:
hook = getattr(cog, f"_{cog.__class__.__name__}__red_permissions_before")
except AttributeError:
pass
else:
self.remove_permissions_hook(hook)
super().remove_cog(cogname) super().remove_cog(cogname)
@ -390,10 +384,17 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
) )
if not hasattr(cog, "requires"): if not hasattr(cog, "requires"):
commands.Cog.__init__(cog) commands.Cog.__init__(cog)
for cls in inspect.getmro(cog.__class__):
try:
hook = getattr(cog, f"_{cls.__name__}__permissions_hook")
except AttributeError:
pass
else:
self.add_permissions_hook(hook)
for attr in dir(cog): for attr in dir(cog):
_attr = getattr(cog, attr) _attr = getattr(cog, attr)
if attr == f"_{cog.__class__.__name__}__permissions_hook":
self.add_permissions_hook(_attr)
if isinstance(_attr, discord.ext.commands.Command) and not isinstance( if isinstance(_attr, discord.ext.commands.Command) and not isinstance(
_attr, commands.Command _attr, commands.Command
): ):

View File

@ -1,4 +1,11 @@
import importlib.machinery import importlib.machinery
from typing import Optional
import discord
from .i18n import Translator
_ = Translator(__name__, __file__)
class RedError(Exception): class RedError(Exception):
@ -21,3 +28,24 @@ class CogLoadError(RedError):
The message will be send to the user.""" The message will be send to the user."""
pass pass
class BankError(RedError):
"""Base error class for bank-related errors."""
class BalanceTooHigh(BankError, OverflowError):
"""Raised when trying to set a user's balance to higher than the maximum."""
def __init__(
self, user: discord.abc.User, max_balance: int, currency_name: str, *args, **kwargs
):
super().__init__(*args, **kwargs)
self.user = user
self.max_balance = max_balance
self.currency_name = currency_name
def __str__(self) -> str:
return _("{user}'s balance cannot rise above {max:,} {currency}.").format(
user=self.user, max=self.max_balance, currency=self.currency_name
)