2020-01-20 20:21:38 +13:00
|
|
|
# redbot/discord
|
|
|
|
from redbot.core.utils.chat_formatting import *
|
|
|
|
from redbot.core.utils import mod
|
|
|
|
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
|
|
|
|
from redbot.core import Config, checks, commands, modlog
|
|
|
|
import discord
|
|
|
|
|
|
|
|
from .utils import *
|
|
|
|
from .memoizer import Memoizer
|
|
|
|
|
|
|
|
# general
|
|
|
|
import asyncio
|
|
|
|
from datetime import datetime
|
|
|
|
import inspect
|
|
|
|
import logging
|
|
|
|
import time
|
|
|
|
import textwrap
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
log = logging.getLogger("red.punish")
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
__version__ = "3.0.0"
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
PURGE_MESSAGES = 1 # for cpunish
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
DEFAULT_ROLE_NAME = "Punished"
|
2020-01-20 20:21:38 +13:00
|
|
|
DEFAULT_TEXT_OVERWRITE = discord.PermissionOverwrite(send_messages=False, send_tts_messages=False, add_reactions=False)
|
|
|
|
DEFAULT_VOICE_OVERWRITE = discord.PermissionOverwrite(speak=False, connect=False)
|
2020-02-16 09:51:27 +13:00
|
|
|
DEFAULT_TIMEOUT_OVERWRITE = discord.PermissionOverwrite(
|
|
|
|
send_messages=True, read_messages=True, read_message_history=True
|
|
|
|
)
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
QUEUE_TIME_CUTOFF = 30
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
DEFAULT_TIMEOUT = "5m"
|
|
|
|
DEFAULT_CASE_MIN_LENGTH = "5m" # only create modlog cases when length is longer than this
|
|
|
|
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
class Punish(commands.Cog):
|
|
|
|
"""
|
|
|
|
Put misbehaving users in timeout where they are unable to speak, read, or
|
|
|
|
do other things that can be denied using discord permissions. Includes
|
|
|
|
auto-setup and more.
|
|
|
|
"""
|
2020-01-28 09:04:50 +13:00
|
|
|
|
2020-01-20 20:21:38 +13:00
|
|
|
def __init__(self, bot):
|
|
|
|
super().__init__()
|
|
|
|
|
|
|
|
self.bot = bot
|
|
|
|
self.config = Config.get_conf(self, identifier=1574368792)
|
|
|
|
# config
|
|
|
|
default_guild = {
|
|
|
|
"PUNISHED": {},
|
|
|
|
"CASE_MIN_LENGTH": parse_time(DEFAULT_CASE_MIN_LENGTH),
|
|
|
|
"PENDING_UNMUTE": [],
|
|
|
|
"REMOVE_ROLE_LIST": [],
|
|
|
|
"TEXT_OVERWRITE": overwrite_to_dict(DEFAULT_TEXT_OVERWRITE),
|
|
|
|
"VOICE_OVERWRITE": overwrite_to_dict(DEFAULT_VOICE_OVERWRITE),
|
|
|
|
"ROLE_ID": None,
|
|
|
|
"NITRO_ID": None,
|
2020-01-28 09:04:50 +13:00
|
|
|
"CHANNEL_ID": None,
|
2020-01-20 20:21:38 +13:00
|
|
|
}
|
|
|
|
self.config.register_guild(**default_guild)
|
|
|
|
|
|
|
|
# queue variables
|
2020-03-22 19:04:29 +13:00
|
|
|
self.queue = asyncio.PriorityQueue()
|
|
|
|
self.queue_lock = asyncio.Lock()
|
2020-01-20 20:21:38 +13:00
|
|
|
self.pending = {}
|
|
|
|
self.enqueued = set()
|
|
|
|
|
2020-03-22 19:04:29 +13:00
|
|
|
self.task = asyncio.create_task(self.on_load())
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
def cog_unload(self):
|
|
|
|
self.task.cancel()
|
|
|
|
|
|
|
|
async def initialize(self):
|
|
|
|
await self.register_casetypes()
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
async def register_casetypes():
|
|
|
|
# register mod case
|
|
|
|
punish_case = {
|
|
|
|
"name": "Timed Mute",
|
|
|
|
"default_setting": True,
|
|
|
|
"image": "\N{HOURGLASS WITH FLOWING SAND}\N{SPEAKER WITH CANCELLATION STROKE}",
|
|
|
|
"case_str": "Timed Mute",
|
|
|
|
}
|
|
|
|
try:
|
|
|
|
await modlog.register_casetype(**punish_case)
|
|
|
|
except RuntimeError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
@commands.group(invoke_without_command=True)
|
|
|
|
@commands.guild_only()
|
|
|
|
@checks.mod()
|
|
|
|
async def punish(self, ctx, user: discord.Member, duration: str = None, *, reason: str = None):
|
|
|
|
"""
|
|
|
|
Puts a user into timeout for a specified time, with optional reason.
|
|
|
|
|
|
|
|
Time specification is any combination of number with the units s,m,h,d,w.
|
|
|
|
Example: !punish @idiot 1.1h10m Breaking rules
|
|
|
|
"""
|
|
|
|
if ctx.invoked_subcommand:
|
|
|
|
return
|
|
|
|
elif user:
|
|
|
|
await self._punish_cmd_common(ctx, user, duration, reason)
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
@punish.command(name="cstart")
|
2020-01-20 20:21:38 +13:00
|
|
|
@commands.guild_only()
|
|
|
|
@checks.mod()
|
|
|
|
async def punish_cstart(self, ctx, user: discord.Member, duration: str = None, *, reason: str = None):
|
|
|
|
"""
|
|
|
|
Same as [p]punish start, but cleans up the target's last message.
|
|
|
|
"""
|
|
|
|
|
|
|
|
success = await self._punish_cmd_common(ctx, user, duration, reason, quiet=True)
|
|
|
|
|
|
|
|
if not success:
|
|
|
|
return
|
|
|
|
|
|
|
|
def check(m):
|
|
|
|
return m.id == ctx.message.id or m.author == user
|
|
|
|
|
|
|
|
try:
|
|
|
|
await ctx.message.channel.purge(limit=PURGE_MESSAGES + 1, check=check)
|
|
|
|
except discord.errors.Forbidden:
|
|
|
|
await ctx.send("Punishment set, but I need permissions to manage messages to clean up.")
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
@punish.command(name="list")
|
2020-01-20 20:21:38 +13:00
|
|
|
@commands.guild_only()
|
|
|
|
@checks.mod()
|
|
|
|
async def punish_list(self, ctx):
|
|
|
|
"""
|
|
|
|
Shows a table of punished users with time, mod and reason.
|
|
|
|
|
|
|
|
Displays punished users, time remaining, responsible moderator and
|
|
|
|
the reason for punishment, if any.
|
|
|
|
"""
|
|
|
|
|
|
|
|
guild = ctx.guild
|
|
|
|
guild_id = guild.id
|
|
|
|
now = time.time()
|
2020-01-28 09:04:50 +13:00
|
|
|
headers = ["Member", "Remaining", "Moderator", "Reason"]
|
2020-01-20 20:21:38 +13:00
|
|
|
punished = await self.config.guild(guild).PUNISHED()
|
|
|
|
|
|
|
|
embeds = []
|
2020-01-21 06:39:36 +13:00
|
|
|
num_p = len(punished)
|
|
|
|
for i, data in enumerate(punished.items()):
|
|
|
|
member_id, data = data
|
2020-01-20 20:21:38 +13:00
|
|
|
member_name = getmname(member_id, guild)
|
2020-01-28 09:04:50 +13:00
|
|
|
moderator = getmname(data["by"], guild)
|
|
|
|
reason = data["reason"]
|
|
|
|
until = data["until"]
|
2020-01-20 20:21:38 +13:00
|
|
|
sort = until or float("inf")
|
2020-01-28 09:04:50 +13:00
|
|
|
remaining = generate_timespec(until - now, short=True) if until else "forever"
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
row = [member_name, remaining, moderator, reason or "No reason set."]
|
2020-01-20 20:21:38 +13:00
|
|
|
embed = discord.Embed(title="Punish List", colour=discord.Colour.from_rgb(255, 0, 0))
|
|
|
|
|
|
|
|
for header, row_val in zip(headers, row):
|
|
|
|
embed.add_field(name=header, value=row_val)
|
|
|
|
|
2020-01-21 06:39:36 +13:00
|
|
|
embed.set_footer(text=f"Page {i+1} out of {num_p}")
|
2020-01-20 20:21:38 +13:00
|
|
|
embeds.append(embed)
|
|
|
|
|
|
|
|
if not punished:
|
|
|
|
await ctx.send("No users are currently punished.")
|
|
|
|
return
|
|
|
|
|
|
|
|
await menu(ctx, embeds, DEFAULT_CONTROLS)
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
@punish.command(name="clean")
|
2020-01-20 20:21:38 +13:00
|
|
|
@commands.guild_only()
|
|
|
|
@checks.mod()
|
|
|
|
async def punish_clean(self, ctx, clean_pending: bool = False):
|
|
|
|
"""
|
|
|
|
Removes absent members from the punished list.
|
|
|
|
|
|
|
|
If run without an argument, it only removes members who are no longer
|
|
|
|
present but whose timer has expired. If the argument is 'yes', 1,
|
|
|
|
or another trueish value, it will also remove absent members whose
|
|
|
|
timers have yet to expire.
|
|
|
|
|
|
|
|
Use this option with care, as removing them will prevent the punished
|
|
|
|
role from being re-added if they rejoin before their timer expires.
|
|
|
|
"""
|
|
|
|
|
|
|
|
count = 0
|
|
|
|
now = time.time()
|
|
|
|
guild = ctx.guild
|
|
|
|
data = await self.config.guild(guild).PUNISHED()
|
|
|
|
|
|
|
|
for mid, mdata in data.copy().items():
|
|
|
|
if not mid.isdigit() or guild.get_member(mid):
|
|
|
|
continue
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
elif clean_pending or ((mdata["until"] or 0) < now):
|
|
|
|
del data[mid]
|
2020-01-20 20:21:38 +13:00
|
|
|
count += 1
|
|
|
|
|
|
|
|
await self.config.guild(guild).PUNISHED.set(data)
|
2020-01-28 09:04:50 +13:00
|
|
|
await ctx.send("Cleaned %i absent members from the list." % count)
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
@punish.command(name="clean-bans")
|
2020-01-20 20:21:38 +13:00
|
|
|
@commands.guild_only()
|
|
|
|
@checks.mod()
|
|
|
|
async def punish_clean_bans(self, ctx):
|
|
|
|
"""
|
|
|
|
Removes banned members from the punished list.
|
|
|
|
"""
|
|
|
|
|
|
|
|
count = 0
|
|
|
|
guild = ctx.guild
|
|
|
|
data = await self.config.guild(guild).PUNISHED()
|
|
|
|
|
|
|
|
try:
|
|
|
|
bans = await guild.bans()
|
|
|
|
ban_ids = {u.id for u in bans}
|
|
|
|
except discord.errors.Forbidden:
|
|
|
|
await ctx.send(warning("I need ban permissions to see the list of banned users."))
|
|
|
|
return
|
|
|
|
|
|
|
|
for mid, mdata in data.copy().items():
|
|
|
|
if not mid.isdigit() or guild.get_member(mid):
|
|
|
|
continue
|
|
|
|
|
|
|
|
elif mid in ban_ids:
|
2020-01-28 09:04:50 +13:00
|
|
|
del data[mid]
|
2020-01-20 20:21:38 +13:00
|
|
|
count += 1
|
|
|
|
|
|
|
|
await self.config.guild(guild).PUNISHED.set(data)
|
2020-01-28 09:04:50 +13:00
|
|
|
await ctx.send("Cleaned %i banned users from the list." % count)
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
@punish.command(name="warn")
|
2020-01-20 20:21:38 +13:00
|
|
|
@commands.guild_only()
|
|
|
|
@checks.mod_or_permissions(manage_messages=True)
|
|
|
|
async def punish_warn(self, ctx, user: discord.Member, *, reason: str = None):
|
|
|
|
"""
|
|
|
|
Warns a user with boilerplate about the rules
|
|
|
|
"""
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
msg = ["Hey %s, " % user.mention]
|
|
|
|
msg.append("you're doing something that might get you muted if you keep " "doing it.")
|
2020-01-20 20:21:38 +13:00
|
|
|
if reason:
|
|
|
|
msg.append(" Specifically, %s." % reason)
|
|
|
|
|
|
|
|
msg.append("Be sure to review the guild rules.")
|
2020-01-28 09:04:50 +13:00
|
|
|
await ctx.send(" ".join(msg))
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
@punish.command(name="end", aliases=["remove"])
|
2020-01-20 20:21:38 +13:00
|
|
|
@commands.guild_only()
|
|
|
|
@checks.mod()
|
|
|
|
async def punish_end(self, ctx, user: discord.Member, *, reason: str = None):
|
|
|
|
"""
|
|
|
|
Removes punishment from a user before time has expired
|
|
|
|
|
|
|
|
This is the same as removing the role directly.
|
|
|
|
"""
|
|
|
|
|
|
|
|
role = await self.get_role(user.guild, quiet=True)
|
|
|
|
sid = user.guild.id
|
|
|
|
guild = user.guild
|
|
|
|
moderator = ctx.author
|
|
|
|
now = time.time()
|
|
|
|
punished = await self.config.guild(guild).PUNISHED()
|
|
|
|
data = punished.get(str(user.id), {})
|
2020-01-28 09:04:50 +13:00
|
|
|
removed_roles_parsed = resolve_role_list(guild, data.get("removed_roles", []))
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
if role and role in user.roles:
|
2020-01-28 09:04:50 +13:00
|
|
|
msg = "Punishment manually ended early by %s." % ctx.author
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
original_start = data.get("start")
|
|
|
|
original_end = data.get("until")
|
2020-01-20 20:21:38 +13:00
|
|
|
remaining = original_end and (original_end - now)
|
|
|
|
|
|
|
|
if remaining:
|
2020-01-28 09:04:50 +13:00
|
|
|
msg += " %s was left" % generate_timespec(round(remaining))
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
if original_start:
|
2020-01-28 09:04:50 +13:00
|
|
|
msg += " of the original %s." % generate_timespec(round(original_end - original_start))
|
2020-01-20 20:21:38 +13:00
|
|
|
else:
|
2020-01-28 09:04:50 +13:00
|
|
|
msg += "."
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
if reason:
|
2020-01-28 09:04:50 +13:00
|
|
|
msg += "\n\nReason for ending early: " + reason
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
if data.get("reason"):
|
|
|
|
msg += "\n\nOriginal reason was: " + data["reason"]
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
updated_reason = str(msg) # copy string
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
if removed_roles_parsed:
|
|
|
|
names_list = format_list(*(r.name for r in removed_roles_parsed))
|
|
|
|
msg += "\nRestored role(s): {}".format(names_list)
|
|
|
|
|
|
|
|
if not await self._unpunish(user, reason=updated_reason, update=True, moderator=moderator):
|
2020-01-28 09:04:50 +13:00
|
|
|
msg += "\n\n(failed to send punishment end notification DM)"
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
await ctx.send(msg)
|
|
|
|
elif data: # This shouldn't happen, but just in case
|
|
|
|
now = time.time()
|
2020-01-28 09:04:50 +13:00
|
|
|
until = data.get("until")
|
|
|
|
remaining = until and generate_timespec(round(until - now)) or "forever"
|
|
|
|
|
|
|
|
data_fmt = "\n".join(
|
|
|
|
[
|
|
|
|
"**Reason:** %s" % (data.get("reason") or "no reason set"),
|
|
|
|
"**Time remaining:** %s" % remaining,
|
|
|
|
"**Moderator**: %s" % (user.guild.get_member(data.get("by")) or "Missing ID#%s" % data.get("by")),
|
|
|
|
]
|
|
|
|
)
|
|
|
|
del punished[str(user.id)]
|
2020-01-20 20:21:38 +13:00
|
|
|
await self.config.guild(guild).PUNISHED.set(punished)
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
await ctx.send(
|
|
|
|
"That user doesn't have the %s role, but they still have a data entry. I removed it, "
|
|
|
|
"but in case it's needed, this is what was there:\n\n%s" % (role.name, data_fmt)
|
|
|
|
)
|
2020-01-20 20:21:38 +13:00
|
|
|
elif role:
|
|
|
|
await ctx.send("That user doesn't have the %s role." % role.name)
|
|
|
|
else:
|
|
|
|
await ctx.send("The punish role couldn't be found in this guild.")
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
@punish.command(name="reason")
|
2020-01-20 20:21:38 +13:00
|
|
|
@commands.guild_only()
|
|
|
|
@checks.mod()
|
|
|
|
async def punish_reason(self, ctx, user: discord.Member, *, reason: str = None):
|
|
|
|
"""
|
|
|
|
Updates the reason for a punishment, including the modlog if a case exists.
|
|
|
|
"""
|
|
|
|
guild = ctx.guild
|
|
|
|
punished = await self.config.guild(guild).PUNISHED()
|
|
|
|
data = punished.get(str(user.id), None)
|
|
|
|
|
|
|
|
if not data:
|
2020-01-28 09:04:50 +13:00
|
|
|
await ctx.send(
|
|
|
|
"That user doesn't have an active punishment entry. To update modlog "
|
|
|
|
"cases manually, use the `%sreason` command." % ctx.prefix
|
|
|
|
)
|
2020-01-20 20:21:38 +13:00
|
|
|
return
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
punished[str(user.id)]["reason"] = reason
|
2020-01-20 20:21:38 +13:00
|
|
|
await self.config.guild(guild).PUNISHED.set(punished)
|
|
|
|
|
|
|
|
if reason:
|
2020-01-28 09:04:50 +13:00
|
|
|
msg = "Reason updated."
|
2020-01-20 20:21:38 +13:00
|
|
|
else:
|
2020-01-28 09:04:50 +13:00
|
|
|
msg = "Reason cleared."
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
caseno = data.get("caseno")
|
2020-01-20 20:21:38 +13:00
|
|
|
try:
|
|
|
|
case = await modlog.get_case(caseno, guild, self.bot)
|
|
|
|
except:
|
|
|
|
msg += "\nMod case not found!"
|
|
|
|
case = None
|
|
|
|
|
|
|
|
if case:
|
|
|
|
moderator = ctx.author
|
|
|
|
|
|
|
|
try:
|
2020-01-28 09:04:50 +13:00
|
|
|
edits = {"reason": reason}
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
if moderator.id != data.get("by"):
|
|
|
|
edits["amended_by"] = moderator
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
edits["modified_at"] = ctx.message.created_at.timestamp()
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
await case.edit(edits)
|
|
|
|
except:
|
|
|
|
msg += "\n" + warning("Mod case not modified due to error.")
|
|
|
|
|
|
|
|
await ctx.send(msg)
|
|
|
|
|
|
|
|
@commands.group()
|
|
|
|
@commands.guild_only()
|
|
|
|
@checks.admin_or_permissions(administrator=True)
|
|
|
|
async def punishset(self, ctx):
|
|
|
|
pass
|
|
|
|
|
|
|
|
@punishset.command(name="remove-roles")
|
|
|
|
async def punishset_remove_role_list(self, ctx, *, rolelist=None):
|
|
|
|
"""Set what roles to remove when punishing.
|
|
|
|
|
|
|
|
COMMA SEPARATED LIST (e.g. Admin,Staff,Mod), Can also use role IDs as well.
|
|
|
|
|
|
|
|
To get current remove role list, run command with no roles.
|
|
|
|
|
|
|
|
Add role_list_clear as the role to clear the guild's remove role list.
|
|
|
|
"""
|
|
|
|
guild = ctx.guild
|
|
|
|
role_list = await self.config.guild(guild).REMOVE_ROLE_LIST()
|
|
|
|
punished = await self.config.guild(guild).PUNISHED()
|
|
|
|
current_roles = resolve_role_list(guild, role_list)
|
|
|
|
|
|
|
|
if rolelist is None:
|
|
|
|
if current_roles:
|
|
|
|
names_list = format_list(*(r.name for r in current_roles))
|
|
|
|
await ctx.send(f"Current list of roles removed when a user is punished: {names_list}")
|
|
|
|
else:
|
|
|
|
await ctx.send("No roles defined for removal.")
|
|
|
|
return
|
|
|
|
elif "role_list_clear" in rolelist.lower():
|
|
|
|
await ctx.send("Remove role list cleared.")
|
|
|
|
await self.config.guild(guild).REMOVE_ROLE_LIST.set([])
|
|
|
|
return
|
|
|
|
|
|
|
|
found_roles = set()
|
|
|
|
notfound_names = set()
|
|
|
|
punish_role = await self.get_role(guild, quiet=True)
|
|
|
|
|
|
|
|
for lookup in rolelist.split(","):
|
|
|
|
lookup = lookup.strip()
|
|
|
|
role = role_from_string(guild, lookup)
|
|
|
|
|
|
|
|
if role:
|
|
|
|
found_roles.add(role)
|
|
|
|
else:
|
|
|
|
notfound_names.add(lookup)
|
|
|
|
|
|
|
|
if notfound_names:
|
|
|
|
fmt_list = format_list(*("`{}`".format(x) for x in notfound_names))
|
|
|
|
await ctx.send(warning(f"These roles were not found: {fmt_list}\n\nPlease try again."))
|
|
|
|
elif punish_role and punish_role in found_roles:
|
|
|
|
await ctx.send(warning("The punished role cannot be removed.\n\nPlease try again."))
|
|
|
|
elif guild.default_role in found_roles:
|
|
|
|
await ctx.send(warning("The everyone role cannot be removed.\n\nPlease try again."))
|
|
|
|
elif found_roles == set(current_roles):
|
|
|
|
await ctx.send("No changes to make.")
|
|
|
|
else:
|
|
|
|
if punished:
|
|
|
|
extra = f"\n\nRun `{ctx.prefix}punishset sync-roles` to apply the changes to punished members."
|
|
|
|
else:
|
|
|
|
extra = ""
|
|
|
|
|
|
|
|
too_high = {r for r in found_roles if r > guild.me.top_role}
|
|
|
|
|
|
|
|
if too_high:
|
|
|
|
fmt_list = format_list(*(r.name for r in too_high))
|
|
|
|
extra += "\n\n" + warning(
|
|
|
|
"These roles are too high for me to manage, and cannot be autoremoved until "
|
|
|
|
f"they are moved under my highest role ({guild.me.top_role}): {fmt_list}."
|
|
|
|
)
|
|
|
|
|
|
|
|
await self.config.guild(guild).REMOVE_ROLE_LIST.set([r.id for r in found_roles])
|
|
|
|
|
|
|
|
fmt_list = format_list(*(r.name for r in found_roles))
|
|
|
|
await ctx.send(f"Will remove these roles when a user is punished: {fmt_list}.{extra}")
|
|
|
|
|
|
|
|
@punishset.command(name="nitro-role")
|
|
|
|
async def punishset_nitro_role(self, ctx, *, role: str = None):
|
|
|
|
"""
|
|
|
|
Set nitro booster role so its not removed when punishing.
|
|
|
|
If your server doesn't have a nitro role, run this command with the role string `no_nitro_role`
|
|
|
|
"""
|
|
|
|
guild = ctx.guild
|
|
|
|
current = await self.config.guild(guild).NITRO_ID()
|
|
|
|
current = role_from_string(guild, current)
|
|
|
|
|
|
|
|
if role and role.lower() == "no_nitro_role":
|
|
|
|
await self.config.guild(guild).NITRO_ID.set(role)
|
|
|
|
await ctx.send("No nitro role set.")
|
|
|
|
return
|
|
|
|
|
|
|
|
if not role and current:
|
|
|
|
await ctx.send(f"Nitro role set to {current}")
|
|
|
|
return
|
|
|
|
elif not role:
|
|
|
|
await ctx.send("No nitro role defined.")
|
|
|
|
return
|
|
|
|
|
|
|
|
role = role_from_string(guild, role)
|
|
|
|
if not role:
|
|
|
|
await ctx.send("Role not found!")
|
|
|
|
return
|
|
|
|
|
|
|
|
await self.config.guild(guild).NITRO_ID.set(role.id)
|
|
|
|
await ctx.send("Nitro role set!")
|
|
|
|
|
|
|
|
@punishset.command(name="sync-roles")
|
|
|
|
async def punishset_sync_roles(self, ctx):
|
|
|
|
"""
|
|
|
|
Applies the remove-roles list to all punished users
|
|
|
|
|
|
|
|
This operation may take some time to complete, depending on the number of members.
|
|
|
|
"""
|
|
|
|
guild = ctx.guild
|
|
|
|
punished = await self.config.guild(guild).PUNISHED()
|
|
|
|
remove_roles = await self.config.guild(guild).REMOVE_ROLE_LIST()
|
|
|
|
role_memo = Memoizer(role_from_string, guild)
|
|
|
|
highest_role = guild.me.top_role
|
|
|
|
count = 0
|
|
|
|
errors = 0
|
|
|
|
|
|
|
|
if not guild.me.guild_permissions.manage_roles:
|
|
|
|
await ctx.send(error("I need the Manage Roles permission to do that."))
|
|
|
|
return
|
|
|
|
|
|
|
|
# (re)populate the member cache
|
|
|
|
if guild.large:
|
|
|
|
await self.bot.request_offline_members(guild)
|
|
|
|
|
|
|
|
# Get current set of roles to remove
|
|
|
|
guild_remove_roles = set(role_memo.filter(remove_roles, skip_nulls=True))
|
|
|
|
|
|
|
|
for member_id, member_data in punished.items():
|
|
|
|
|
|
|
|
member = guild.get_member(member_id)
|
|
|
|
|
|
|
|
if not member:
|
|
|
|
continue
|
|
|
|
|
|
|
|
member_roles = set(member.roles)
|
|
|
|
original_roles = member_roles.copy()
|
|
|
|
|
|
|
|
try:
|
|
|
|
# Combine sets to get the baseline (roles they'd have normally)
|
2020-01-28 09:04:50 +13:00
|
|
|
member_roles |= set(role_memo.filter(member_data["removed_roles"], skip_nulls=True))
|
2020-01-20 20:21:38 +13:00
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
# update new removed roles with intersection of guild removal list and baseline
|
|
|
|
new_removed = guild_remove_roles & member_roles
|
2020-01-28 09:04:50 +13:00
|
|
|
punished[str(member.id)]["removed_roles"] = [r.id for r in new_removed]
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
member_roles -= guild_remove_roles
|
|
|
|
|
|
|
|
# can't restore, so skip (remove from set)
|
2020-01-28 09:04:50 +13:00
|
|
|
for role in member_roles - original_roles:
|
2020-01-20 20:21:38 +13:00
|
|
|
if role >= highest_role:
|
|
|
|
member_roles.discard(role)
|
|
|
|
|
|
|
|
# can't remove, so skip (re-add to set)
|
2020-01-28 09:04:50 +13:00
|
|
|
for role in original_roles - member_roles:
|
2020-01-20 20:21:38 +13:00
|
|
|
if role >= highest_role:
|
|
|
|
member_roles.add(role)
|
|
|
|
|
|
|
|
# Now update roles if we need to
|
|
|
|
if member_roles != original_roles:
|
|
|
|
try:
|
|
|
|
await member.edit(roles=member_roles, reason="punish sync roles")
|
|
|
|
except Exception:
|
|
|
|
log.exception(f"Couldn't modify roles in sync-roles command in {guild.name}!")
|
|
|
|
errors += 1
|
|
|
|
else:
|
|
|
|
count += 1
|
|
|
|
|
|
|
|
msg = f"Updated {count} members' roles."
|
|
|
|
|
|
|
|
if errors:
|
|
|
|
msg += "\n" + warning(f"{errors} errors occured; check the bot logs for more information.")
|
|
|
|
|
|
|
|
await ctx.send(msg)
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
@punishset.command(name="setup")
|
2020-01-20 20:21:38 +13:00
|
|
|
async def punishset_setup(self, ctx):
|
|
|
|
"""
|
|
|
|
(Re)configures the punish role and channel overrides
|
|
|
|
"""
|
|
|
|
guild = ctx.guild
|
|
|
|
default_name = DEFAULT_ROLE_NAME
|
|
|
|
role_id = await self.config.guild(guild).ROLE_ID()
|
|
|
|
|
|
|
|
if role_id:
|
|
|
|
role = discord.utils.get(guild.roles, id=role_id)
|
|
|
|
else:
|
|
|
|
role = discord.utils.get(guild.roles, name=default_name)
|
|
|
|
|
|
|
|
perms = guild.me.guild_permissions
|
|
|
|
if not perms.manage_roles and perms.manage_channels:
|
|
|
|
await ctx.send("I need the Manage Roles and Manage Channels permissions for that command to work.")
|
|
|
|
return
|
|
|
|
|
|
|
|
if not role:
|
|
|
|
msg = "The %s role doesn't exist; Creating it now... " % default_name
|
|
|
|
|
|
|
|
msgobj = await ctx.send(msg)
|
|
|
|
|
|
|
|
perms = discord.Permissions.none()
|
|
|
|
role = await guild.create_role(name=default_name, permissions=perms, reason="punish cog.")
|
|
|
|
else:
|
2020-01-28 09:04:50 +13:00
|
|
|
msgobj = await ctx.send("%s role exists... " % role.name)
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
if role.position != (guild.me.top_role.position - 1):
|
|
|
|
if role < guild.me.top_role:
|
2020-01-28 09:04:50 +13:00
|
|
|
await msgobj.edit(content=msgobj.content + "moving role to higher position... ")
|
2020-01-20 20:21:38 +13:00
|
|
|
await role.edit(position=guild.me.top_role.position - 1)
|
|
|
|
else:
|
2020-01-28 09:04:50 +13:00
|
|
|
await msgobj.edit(
|
|
|
|
content=msgobj.content + "role is too high to manage." " Please move it to below my highest role."
|
|
|
|
)
|
2020-01-20 20:21:38 +13:00
|
|
|
return
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
await msgobj.edit(content=msgobj.content + "(re)configuring channels... ")
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
for channel in guild.channels:
|
|
|
|
await self.setup_channel(channel, role)
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
await msgobj.edit(content=msgobj.content + "done.")
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
if role and role.id != role_id:
|
|
|
|
await self.config.guild(guild).ROLE_ID.set(role.id)
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
@punishset.command(name="channel")
|
2020-01-20 20:21:38 +13:00
|
|
|
async def punishset_channel(self, ctx, channel: discord.TextChannel = None):
|
|
|
|
"""
|
|
|
|
Sets or shows the punishment "timeout" channel.
|
|
|
|
|
|
|
|
This channel has special settings to allow punished users to discuss their
|
|
|
|
infraction(s) with moderators.
|
|
|
|
|
|
|
|
If there is a role deny on the channel for the punish role, it is
|
|
|
|
automatically set to allow. If the default permissions don't allow the
|
|
|
|
punished role to see or speak in it, an overwrite is created to allow
|
|
|
|
them to do so.
|
|
|
|
"""
|
|
|
|
guild = ctx.guild
|
|
|
|
current = await self.config.guild(guild).CHANNEL_ID()
|
|
|
|
current = current and guild.get_channel(current)
|
|
|
|
|
|
|
|
if channel is None:
|
|
|
|
if not current:
|
|
|
|
await ctx.send("No timeout channel has been set.")
|
|
|
|
else:
|
|
|
|
await ctx.send("The timeout channel is currently %s." % current.mention)
|
|
|
|
else:
|
|
|
|
if current == channel:
|
2020-01-28 09:04:50 +13:00
|
|
|
await ctx.send(
|
|
|
|
"The timeout channel is already %s. If you need to repair its permissions, use `%spunishset setup`."
|
|
|
|
% (current.mention, ctx.prefix)
|
|
|
|
)
|
2020-01-20 20:21:38 +13:00
|
|
|
return
|
|
|
|
|
|
|
|
await self.config.guild(guild).CHANNEL_ID.set(channel.id)
|
|
|
|
|
|
|
|
role = await self.get_role(guild, create=True)
|
2020-01-28 09:04:50 +13:00
|
|
|
update_msg = "{} to the %s role" % role
|
2020-01-20 20:21:38 +13:00
|
|
|
grants = []
|
|
|
|
denies = []
|
|
|
|
perms = permissions_for_roles(channel, role)
|
|
|
|
overwrite = channel.overwrites_for(role) or discord.PermissionOverwrite()
|
|
|
|
|
|
|
|
for perm, value in DEFAULT_TIMEOUT_OVERWRITE:
|
|
|
|
if value is None:
|
|
|
|
continue
|
|
|
|
|
|
|
|
if getattr(perms, perm) != value:
|
|
|
|
setattr(overwrite, perm, value)
|
2020-01-28 09:04:50 +13:00
|
|
|
name = perm.replace("_", " ").title().replace("Tts", "TTS")
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
if value:
|
|
|
|
grants.append(name)
|
|
|
|
else:
|
|
|
|
denies.append(name)
|
|
|
|
|
|
|
|
# Any changes made? Apply them.
|
|
|
|
if grants or denies:
|
2020-01-28 09:04:50 +13:00
|
|
|
grants = grants and ("grant " + format_list(*grants))
|
|
|
|
denies = denies and ("deny " + format_list(*denies))
|
2020-01-20 20:21:38 +13:00
|
|
|
to_join = [x for x in (grants, denies) if x]
|
|
|
|
update_msg = update_msg.format(format_list(*to_join))
|
|
|
|
|
|
|
|
if current and current.id != channel.id:
|
|
|
|
if current.permissions_for(guild.me).manage_roles:
|
|
|
|
msg = info("Resetting permissions in the old channel (%s) to the default...")
|
|
|
|
else:
|
|
|
|
msg = error("I don't have permissions to reset permissions in the old channel (%s)")
|
|
|
|
|
|
|
|
await ctx.send(msg % current.mention)
|
|
|
|
await self.setup_channel(current, role)
|
|
|
|
|
|
|
|
if channel.permissions_for(guild.me).manage_roles:
|
2020-01-28 09:04:50 +13:00
|
|
|
await ctx.send(info("Updating permissions in %s to %s..." % (channel.mention, update_msg)))
|
2020-01-20 20:21:38 +13:00
|
|
|
await channel.set_permissions(role, overwrite=overwrite)
|
|
|
|
else:
|
|
|
|
await ctx.send(error("I don't have permissions to %s." % update_msg))
|
|
|
|
|
|
|
|
await ctx.send("Timeout channel set to %s." % channel.mention)
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
@punishset.command(name="clear-channel")
|
2020-01-20 20:21:38 +13:00
|
|
|
async def punishset_clear_channel(self, ctx):
|
|
|
|
"""
|
|
|
|
Clears the timeout channel and resets its permissions
|
|
|
|
"""
|
|
|
|
guild = ctx.guild
|
|
|
|
current = await self.config.guild(guild).CHANNEL_ID()
|
|
|
|
current = current and guild.get_channel(current)
|
|
|
|
|
|
|
|
if current:
|
|
|
|
msg = None
|
|
|
|
await self.config.guild(guild).CHANNEL_ID.set(None)
|
|
|
|
|
|
|
|
if current.permissions_for(guild.me).manage_roles:
|
|
|
|
role = await self.get_role(guild, quiet=True)
|
|
|
|
await self.setup_channel(current, role)
|
2020-01-28 09:04:50 +13:00
|
|
|
msg = " and its permissions reset"
|
2020-01-20 20:21:38 +13:00
|
|
|
else:
|
|
|
|
msg = ", but I don't have permissions to reset its permissions."
|
|
|
|
|
|
|
|
await ctx.send("Timeout channel has been cleared%s." % msg)
|
|
|
|
else:
|
|
|
|
await ctx.send("No timeout channel has been set yet.")
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
@punishset.command(name="case-min")
|
2020-01-20 20:21:38 +13:00
|
|
|
async def punishset_case_min(self, ctx, *, timespec: str = None):
|
|
|
|
"""
|
|
|
|
Set/disable or display the minimum punishment case duration
|
|
|
|
|
|
|
|
If the punishment duration is less than this value, a case will not be created.
|
|
|
|
Specify 'disable' to turn off case creation altogether.
|
|
|
|
"""
|
|
|
|
guild = ctx.guild
|
|
|
|
current = await self.config.guild(guild).CASE_MIN_LENGTH()
|
|
|
|
|
|
|
|
if not timespec:
|
|
|
|
if current:
|
2020-01-28 09:04:50 +13:00
|
|
|
await ctx.send("Punishments longer than %s will create cases." % generate_timespec(current))
|
2020-01-20 20:21:38 +13:00
|
|
|
else:
|
|
|
|
await ctx.send("Punishment case creation is disabled.")
|
|
|
|
else:
|
2020-01-28 09:04:50 +13:00
|
|
|
if timespec.strip("'\"").lower() == "disable":
|
2020-01-20 20:21:38 +13:00
|
|
|
value = None
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
value = parse_time(timespec)
|
|
|
|
except BadTimeExpr as e:
|
|
|
|
await ctx.send(error(e.args[0]))
|
|
|
|
return
|
|
|
|
|
|
|
|
await self.config.guild(guild).CASE_MIN_LENGTH.set(value)
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
await ctx.send("Punishments longer than %s will create cases." % generate_timespec(value))
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
@punishset.command(name="overrides")
|
2020-01-20 20:21:38 +13:00
|
|
|
async def punishset_overrides(self, ctx, *, channel_id: int = None):
|
|
|
|
"""
|
|
|
|
Copy or display the punish role overrides
|
|
|
|
|
|
|
|
If a channel id is specified, the allow/deny settings for it are saved
|
|
|
|
and applied to new channels when they are created. To apply the new
|
|
|
|
settings to existing channels, use [p]punishset setup.
|
|
|
|
|
|
|
|
An important caveat: voice channel and text channel overrides are
|
|
|
|
configured separately! To set the overrides for a channel type,
|
|
|
|
specify the name of or mention a channel of that type.
|
|
|
|
"""
|
|
|
|
|
|
|
|
guild = ctx.guild
|
|
|
|
role = await self.get_role(guild, quiet=True)
|
|
|
|
timeout_channel_id = await self.config.guild(guild).CHANNEL_ID()
|
|
|
|
confirm_msg = None
|
|
|
|
channel = guild.get_channel(channel_id)
|
|
|
|
|
|
|
|
if not role:
|
|
|
|
await ctx.send(error("Punish role has not been created yet. Run `%spunishset setup` first." % ctx.prefix))
|
|
|
|
return
|
|
|
|
|
|
|
|
if channel:
|
|
|
|
overwrite = channel.overwrites_for(role)
|
|
|
|
if channel.id == timeout_channel_id:
|
|
|
|
confirm_msg = "Are you sure you want to copy overrides from the timeout channel?"
|
|
|
|
elif overwrite is None:
|
|
|
|
overwrite = discord.PermissionOverwrite()
|
|
|
|
confirm_msg = "Are you sure you want to copy blank (no permissions set) overrides?"
|
|
|
|
else:
|
|
|
|
confirm_msg = "Are you sure you want to copy overrides from this channel?"
|
|
|
|
|
|
|
|
if channel.type is discord.ChannelType.text:
|
2020-01-28 09:04:50 +13:00
|
|
|
key = "text"
|
2020-01-20 20:21:38 +13:00
|
|
|
elif channel.type is discord.ChannelType.voice:
|
2020-01-28 09:04:50 +13:00
|
|
|
key = "voice"
|
2020-01-20 20:21:38 +13:00
|
|
|
else:
|
|
|
|
await ctx.send(error("Unknown channel type!"))
|
|
|
|
return
|
|
|
|
|
|
|
|
if confirm_msg:
|
2020-01-28 09:04:50 +13:00
|
|
|
await ctx.send(warning(confirm_msg + "(reply `yes` within 30s to confirm)"))
|
|
|
|
|
2020-01-20 20:21:38 +13:00
|
|
|
def check(m):
|
|
|
|
return m.author == ctx.author and m.channel == ctx.channel
|
2020-01-28 09:04:50 +13:00
|
|
|
|
2020-01-20 20:21:38 +13:00
|
|
|
try:
|
2020-01-28 09:04:50 +13:00
|
|
|
reply = await self.bot.wait_for("message", check=check, timeout=30.0)
|
|
|
|
if reply.content.strip(" `\"'").lower() != "yes":
|
|
|
|
await ctx.send("Commmand cancelled.")
|
2020-01-20 20:21:38 +13:00
|
|
|
return
|
|
|
|
except asyncio.TimeoutError:
|
2020-01-28 09:04:50 +13:00
|
|
|
await ctx.send("Timed out waiting for a response.")
|
2020-01-20 20:21:38 +13:00
|
|
|
return
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
if key == "text":
|
2020-01-20 20:21:38 +13:00
|
|
|
await self.config.guild(guild).TEXT_OVERWRITE.set(overwrite_to_dict(overwrite))
|
|
|
|
else:
|
|
|
|
await self.config.guild(guild).VOICE_OVERWRITE.set(overwrite_to_dict(overwrite))
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
await ctx.send(
|
|
|
|
"{} channel overrides set to:\n".format(key.title())
|
|
|
|
+ format_permissions(overwrite)
|
|
|
|
+ "\n\nRun `%spunishset setup` to apply them to all channels." % ctx.prefix
|
|
|
|
)
|
2020-01-20 20:21:38 +13:00
|
|
|
else:
|
|
|
|
msg = []
|
2020-01-28 09:04:50 +13:00
|
|
|
for key in ("text", "voice"):
|
|
|
|
if key == "text":
|
2020-01-20 20:21:38 +13:00
|
|
|
data = await self.config.guild(guild).TEXT_OVERWRITE()
|
|
|
|
else:
|
|
|
|
data = await self.config.guild(guild).VOICE_OVERWRITE()
|
2020-01-28 09:04:50 +13:00
|
|
|
title = "%s permission overrides:" % key.title()
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
if data == overwrite_to_dict(DEFAULT_TEXT_OVERWRITE) or data == overwrite_to_dict(
|
|
|
|
DEFAULT_VOICE_OVERWRITE
|
|
|
|
):
|
|
|
|
title = title[:-1] + " (defaults):"
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
msg.append(bold(title) + "\n" + format_permissions(overwrite_from_dict(data)))
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
await ctx.send("\n\n".join(msg))
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
@punishset.command(name="reset-overrides")
|
|
|
|
async def punishset_reset_overrides(self, ctx, channel_type: str = "both"):
|
2020-01-20 20:21:38 +13:00
|
|
|
"""
|
|
|
|
Resets the punish role overrides for text, voice or both (default)
|
|
|
|
|
|
|
|
This command exists in case you want to restore the default settings
|
|
|
|
for newly created channels.
|
|
|
|
"""
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
channel_type = channel_type.strip("`\"' ").lower()
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
msg = []
|
2020-01-28 09:04:50 +13:00
|
|
|
for key in ("text", "voice"):
|
|
|
|
if channel_type not in ["both", key]:
|
2020-01-20 20:21:38 +13:00
|
|
|
continue
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
title = "%s permission overrides reset to:" % key.title()
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
if key == "text":
|
2020-01-20 20:21:38 +13:00
|
|
|
await self.config.guild(guild).TEXT_OVERWRITE.set(overwrite_to_dict(DEFAULT_TEXT_OVERWRITE))
|
2020-01-28 09:04:50 +13:00
|
|
|
msg.append(bold(title) + "\n" + format_permissions(overwrite_to_dict(DEFAULT_TEXT_OVERWRITE)))
|
2020-01-20 20:21:38 +13:00
|
|
|
else:
|
|
|
|
await self.config.guild(guild).VOICE_OVERWRITE.set(overwrite_to_dict(DEFAULT_VOICE_OVERWRITE))
|
2020-01-28 09:04:50 +13:00
|
|
|
msg.append(bold(title) + "\n" + format_permissions(overwrite_to_dict(DEFAULT_VOICE_OVERWRITE)))
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
if not msg:
|
|
|
|
await ctx.send("Invalid channel type. Use `text`, `voice`, or `both` (the default, if not specified)")
|
|
|
|
return
|
|
|
|
|
|
|
|
msg.append("Run `%spunishset setup` to apply them to all channels." % ctx.prefix)
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
await ctx.send("\n\n".join(msg))
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
async def get_role(self, guild, quiet=False, create=False):
|
|
|
|
role_id = await self.config.guild(guild).ROLE_ID()
|
|
|
|
|
|
|
|
if role_id:
|
|
|
|
role = discord.utils.get(guild.roles, id=role_id)
|
|
|
|
else:
|
|
|
|
role = discord.utils.get(guild.roles, name=DEFAULT_ROLE_NAME)
|
|
|
|
|
|
|
|
if create and not role:
|
|
|
|
perms = guild.me.guild_permissions
|
|
|
|
if not perms.manage_roles and perms.manage_channels:
|
|
|
|
await ctx.send("The Manage Roles and Manage Channels permissions are required to use this command.")
|
|
|
|
return
|
|
|
|
|
|
|
|
else:
|
|
|
|
msg = "The %s role doesn't exist; Creating it now..." % DEFAULT_ROLE_NAME
|
|
|
|
|
|
|
|
if not quiet:
|
|
|
|
msgobj = await ctx.send(msg)
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
log.debug("Creating punish role in %s" % guild.name)
|
2020-01-20 20:21:38 +13:00
|
|
|
perms = discord.Permissions.none()
|
|
|
|
role = await guild.create_role(name=DEFAULT_ROLE_NAME, permissions=perms, reason="punish cog.")
|
|
|
|
await role.edit(position=guild.me.top_role.position - 1)
|
|
|
|
|
|
|
|
if not quiet:
|
2020-01-28 09:04:50 +13:00
|
|
|
await msgobj.edit(content=msgobj.content + "\nconfiguring channels... ")
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
for channel in guild.channels:
|
|
|
|
await self.setup_channel(channel, role)
|
|
|
|
|
|
|
|
if not quiet:
|
2020-01-28 09:04:50 +13:00
|
|
|
await msgobj.edit(content=msgobj.content + "\ndone.")
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
if role and role.id != role_id:
|
|
|
|
await self.config.guild(guild).ROLE_ID.set(role.id)
|
|
|
|
|
|
|
|
return role
|
|
|
|
|
|
|
|
async def setup_channel(self, channel, role):
|
|
|
|
guild = channel.guild
|
|
|
|
timeout_channel_id = await self.config.guild(guild).CHANNEL_ID()
|
|
|
|
|
|
|
|
if channel.id == timeout_channel_id:
|
|
|
|
# maybe this will be used later:
|
|
|
|
# config = settings.get('TIMEOUT_OVERWRITE')
|
|
|
|
config = None
|
|
|
|
defaults = DEFAULT_TIMEOUT_OVERWRITE
|
|
|
|
elif channel.type is discord.ChannelType.voice:
|
|
|
|
config = await self.config.guild(guild).VOICE_OVERWRITE()
|
|
|
|
defaults = DEFAULT_VOICE_OVERWRITE
|
|
|
|
else:
|
|
|
|
config = await self.config.guild(guild).TEXT_OVERWRITE()
|
|
|
|
defaults = DEFAULT_TEXT_OVERWRITE
|
|
|
|
|
|
|
|
if config:
|
|
|
|
perms = overwrite_from_dict(config)
|
|
|
|
else:
|
|
|
|
perms = defaults
|
|
|
|
|
|
|
|
await channel.set_permissions(role, overwrite=perms, reason="punish cog")
|
|
|
|
|
|
|
|
async def on_load(self):
|
|
|
|
await self.bot.wait_until_ready()
|
|
|
|
|
|
|
|
for guild in self.bot.guilds:
|
|
|
|
me = guild.me
|
|
|
|
role = await self.get_role(guild, quiet=True, create=True)
|
|
|
|
|
|
|
|
if not role:
|
|
|
|
log.error("Needed to create punish role in %s, but couldn't." % guild.name)
|
|
|
|
continue
|
|
|
|
|
|
|
|
role_memo = Memoizer(role_from_string, guild)
|
|
|
|
punished = await self.config.guild(guild).PUNISHED()
|
|
|
|
|
|
|
|
for member_id, data in punished.items():
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
until = data["until"]
|
2020-01-20 20:21:38 +13:00
|
|
|
member = guild.get_member(member_id)
|
|
|
|
|
|
|
|
if until and (until - time.time()) < 0:
|
|
|
|
if member:
|
2020-01-28 09:04:50 +13:00
|
|
|
reason = "Punishment removal overdue, maybe the bot was offline. "
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
if data["reason"]:
|
|
|
|
reason += data["reason"]
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
await self._unpunish(member, reason=reason)
|
|
|
|
else: # member disappeared
|
2020-01-28 09:04:50 +13:00
|
|
|
del punished[str(member_id)]
|
2020-01-20 20:21:38 +13:00
|
|
|
elif member:
|
|
|
|
# re-check roles
|
|
|
|
user_roles = set(member.roles)
|
2020-01-28 09:04:50 +13:00
|
|
|
removed_roles = set(role_memo.filter(data.get("removed_roles", ()), skip_nulls=True))
|
2020-01-20 20:21:38 +13:00
|
|
|
removed_roles = user_roles & {r for r in removed_roles if r < me.top_role}
|
|
|
|
user_roles -= removed_roles
|
|
|
|
|
|
|
|
apply_roles = removed_roles
|
|
|
|
|
|
|
|
if role not in user_roles:
|
|
|
|
if role >= me.top_role:
|
|
|
|
log.error("Needed to re-add punish role to %s in %s, but couldn't." % (member, guild.name))
|
|
|
|
else:
|
|
|
|
user_roles.add(role) # add punish role to the set
|
|
|
|
apply_roles = True
|
|
|
|
|
|
|
|
if apply_roles:
|
|
|
|
await member.edit(roles=member_roles, reason="punish ending")
|
|
|
|
|
|
|
|
if until:
|
|
|
|
await self.schedule_unpunish(until, member)
|
|
|
|
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
async with self.queue_lock:
|
|
|
|
while await self.process_queue_event():
|
|
|
|
pass
|
|
|
|
|
|
|
|
await asyncio.sleep(5)
|
|
|
|
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
break
|
|
|
|
except Exception:
|
|
|
|
pass
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
log.debug("queue manager dying")
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
while not self.queue.empty():
|
|
|
|
self.queue.get_nowait()
|
|
|
|
|
|
|
|
for fut in self.pending.values():
|
|
|
|
fut.cancel()
|
|
|
|
|
|
|
|
async def cancel_queue_event(self, *args) -> bool:
|
|
|
|
if args in self.pending:
|
|
|
|
self.pending.pop(args).cancel()
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
events = []
|
|
|
|
removed = None
|
|
|
|
|
|
|
|
async with self.queue_lock:
|
|
|
|
while not self.queue.empty():
|
|
|
|
item = self.queue.get_nowait()
|
|
|
|
|
|
|
|
if args == item[1:]:
|
|
|
|
removed = item
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
events.append(item)
|
|
|
|
|
|
|
|
for item in events:
|
|
|
|
self.queue.put_nowait(item)
|
|
|
|
|
|
|
|
return removed is not None
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
async def put_queue_event(self, run_at: float, *args):
|
2020-01-20 20:21:38 +13:00
|
|
|
diff = run_at - time.time()
|
|
|
|
|
|
|
|
if args in self.enqueued:
|
|
|
|
return False
|
|
|
|
|
|
|
|
self.enqueued.add(args)
|
|
|
|
|
|
|
|
if diff < 0:
|
|
|
|
self.execute_queue_event(*args)
|
|
|
|
elif run_at - time.time() < QUEUE_TIME_CUTOFF:
|
2020-03-22 19:20:49 +13:00
|
|
|
self.pending[args] = asyncio.get_event_loop().call_later(diff, self.execute_queue_event, *args)
|
2020-01-20 20:21:38 +13:00
|
|
|
else:
|
|
|
|
await self.queue.put((run_at, *args))
|
|
|
|
|
|
|
|
async def process_queue_event(self):
|
|
|
|
if self.queue.empty():
|
|
|
|
return False
|
|
|
|
|
|
|
|
now = time.time()
|
|
|
|
item = await self.queue.get()
|
|
|
|
next_time, *args = item
|
|
|
|
|
|
|
|
diff = next_time - now
|
|
|
|
|
|
|
|
if diff < 0:
|
|
|
|
if self.execute_queue_event(*args):
|
|
|
|
return
|
|
|
|
elif diff < QUEUE_TIME_CUTOFF:
|
2020-03-22 19:20:49 +13:00
|
|
|
self.pending[args] = asyncio.get_event_loop().call_later(diff, self.execute_queue_event, *args)
|
2020-01-20 20:21:38 +13:00
|
|
|
return True
|
|
|
|
|
|
|
|
await self.queue.put(item)
|
|
|
|
return False
|
|
|
|
|
|
|
|
def execute_queue_event(self, *args) -> bool:
|
|
|
|
self.enqueued.discard(args)
|
|
|
|
|
|
|
|
try:
|
|
|
|
return self.execute_unpunish(*args)
|
|
|
|
except Exception:
|
|
|
|
log.exception("failed to execute scheduled event")
|
|
|
|
|
|
|
|
async def _punish_cmd_common(self, ctx, member, duration, reason, quiet=False):
|
|
|
|
guild = ctx.guild
|
|
|
|
using_default = False
|
|
|
|
updating_case = False
|
|
|
|
case_error = None
|
|
|
|
|
|
|
|
remove_role_set = await self.config.guild(guild).REMOVE_ROLE_LIST()
|
|
|
|
remove_role_set = set(resolve_role_list(guild, remove_role_set))
|
|
|
|
punished = await self.config.guild(guild).PUNISHED()
|
|
|
|
current = punished.get(str(member.id), {})
|
2020-01-28 09:04:50 +13:00
|
|
|
reason = reason or current.get("reason") # don't clear if not given
|
2020-01-20 20:21:38 +13:00
|
|
|
hierarchy_allowed = ctx.author.top_role > member.top_role
|
|
|
|
case_min_length = await self.config.guild(guild).CASE_MIN_LENGTH()
|
|
|
|
nitro_role = await self.config.guild(guild).NITRO_ID()
|
|
|
|
|
|
|
|
if nitro_role is None:
|
|
|
|
await ctx.send(f"Please set the nitro role using `{ctx.prefix}punishset nitro-role`")
|
|
|
|
return
|
|
|
|
|
|
|
|
if member == guild.me:
|
|
|
|
await ctx.send("You can't punish the bot.")
|
|
|
|
return
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
if duration and duration.lower() in ["forever", "inf", "infinite"]:
|
2020-01-20 20:21:38 +13:00
|
|
|
duration = None
|
|
|
|
else:
|
|
|
|
if not duration:
|
|
|
|
using_default = True
|
|
|
|
duration = DEFAULT_TIMEOUT
|
|
|
|
try:
|
|
|
|
duration = parse_time(duration)
|
|
|
|
if duration < 1:
|
|
|
|
await ctx.send("Duration must be 1 second or longer.")
|
|
|
|
return False
|
|
|
|
except BadTimeExpr as e:
|
|
|
|
await ctx.send("Error parsing duration: %s." % e.args)
|
|
|
|
return False
|
|
|
|
|
|
|
|
role = await self.get_role(guild, quiet=quiet, create=True)
|
|
|
|
|
|
|
|
if role is None:
|
|
|
|
return
|
|
|
|
elif role >= guild.me.top_role:
|
2020-01-28 09:04:50 +13:00
|
|
|
await ctx.send("The %s role is too high for me to manage." % role)
|
2020-01-20 20:21:38 +13:00
|
|
|
return
|
|
|
|
|
|
|
|
# Call time() after getting the role due to potential creation delay
|
|
|
|
now = time.time()
|
|
|
|
until = (now + duration + 0.5) if duration else None
|
|
|
|
duration_ok = (case_min_length is not None) and ((duration is None) or duration >= case_min_length)
|
|
|
|
|
|
|
|
if duration_ok:
|
|
|
|
now_date = datetime.utcfromtimestamp(now)
|
|
|
|
mod_until = until and datetime.utcfromtimestamp(until)
|
|
|
|
|
|
|
|
try:
|
|
|
|
if current:
|
2020-01-28 09:04:50 +13:00
|
|
|
case_number = current.get("caseno")
|
2020-01-20 20:21:38 +13:00
|
|
|
try:
|
|
|
|
case = await modlog.get_case(case_number, guild, self.bot)
|
2020-01-28 09:04:50 +13:00
|
|
|
except: # shouldn't happen
|
|
|
|
await ctx.send(
|
|
|
|
warning(
|
|
|
|
"Error, modlog case not found, but user is punished with case.\nTry unpunishing and punishing again."
|
|
|
|
)
|
|
|
|
)
|
2020-01-20 20:21:38 +13:00
|
|
|
return
|
|
|
|
|
|
|
|
moderator = ctx.author
|
|
|
|
|
|
|
|
try:
|
2020-01-28 09:04:50 +13:00
|
|
|
edits = {"reason": reason}
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
if moderator.id != current.get("by"):
|
|
|
|
edits["amended_by"] = moderator
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
edits["modified_at"] = ctx.message.created_at.timestamp()
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
await case.edit(edits)
|
|
|
|
except Exception as e:
|
|
|
|
await ctx.send(warning(f"Couldn't edit case: {e}"))
|
|
|
|
return
|
|
|
|
|
|
|
|
updating_case = True
|
|
|
|
|
|
|
|
else:
|
2020-01-28 09:04:50 +13:00
|
|
|
case = await modlog.create_case(
|
|
|
|
self.bot,
|
|
|
|
guild,
|
|
|
|
now_date,
|
|
|
|
"Timed Mute",
|
|
|
|
member,
|
|
|
|
moderator=ctx.author,
|
|
|
|
reason=reason,
|
|
|
|
until=mod_until,
|
|
|
|
)
|
2020-01-20 20:21:38 +13:00
|
|
|
case_number = case.case_number
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
case_error = e
|
|
|
|
else:
|
|
|
|
case_number = None
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
subject = "the %s role" % role.name
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
if str(member.id) in punished:
|
|
|
|
if role in member.roles:
|
2020-01-28 09:04:50 +13:00
|
|
|
msg = "{0} already had the {1.name} role; resetting their timer."
|
2020-01-20 20:21:38 +13:00
|
|
|
else:
|
2020-01-28 09:04:50 +13:00
|
|
|
msg = "{0} is missing the {1.name} role for some reason. I added it and reset their timer."
|
2020-01-20 20:21:38 +13:00
|
|
|
elif role in member.roles:
|
2020-01-28 09:04:50 +13:00
|
|
|
msg = "{0} already had the {1.name} role, but had no timer; setting it now."
|
2020-01-20 20:21:38 +13:00
|
|
|
else:
|
2020-01-28 09:04:50 +13:00
|
|
|
msg = "Applied the {1.name} role to {0}."
|
|
|
|
subject = "it"
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
msg = msg.format(member, role)
|
|
|
|
|
|
|
|
if duration:
|
|
|
|
timespec = generate_timespec(duration)
|
|
|
|
|
|
|
|
if using_default:
|
2020-01-28 09:04:50 +13:00
|
|
|
timespec += " (the default)"
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
msg += " I will remove %s in %s." % (subject, timespec)
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
if case_error:
|
|
|
|
if isinstance(case_error, CaseMessageNotFound):
|
2020-01-28 09:04:50 +13:00
|
|
|
case_error = "the case message could not be found"
|
2020-01-20 20:21:38 +13:00
|
|
|
elif isinstance(case_error, NoModLogAccess):
|
2020-01-28 09:04:50 +13:00
|
|
|
case_error = "I do not have access to the modlog channel"
|
2020-01-20 20:21:38 +13:00
|
|
|
else:
|
|
|
|
case_error = None
|
|
|
|
|
|
|
|
if case_error:
|
2020-01-28 09:04:50 +13:00
|
|
|
verb = "updating" if updating_case else "creating"
|
|
|
|
msg += "\n\n" + warning("There was an error %s the modlog case: %s." % (verb, case_error))
|
2020-01-20 20:21:38 +13:00
|
|
|
elif case_number:
|
2020-01-28 09:04:50 +13:00
|
|
|
verb = "updated" if updating_case else "created"
|
|
|
|
msg += " I also %s case #%i in the modlog." % (verb, case_number)
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
voice_overwrite = await self.config.guild(guild).VOICE_OVERWRITE()
|
|
|
|
|
|
|
|
if voice_overwrite:
|
|
|
|
voice_overwrite = overwrite_from_dict(voice_overwrite)
|
|
|
|
else:
|
|
|
|
voice_overwrite = DEFAULT_VOICE_OVERWRITE
|
|
|
|
|
2020-02-09 13:42:43 +13:00
|
|
|
voice_deny = voice_overwrite.pair()[1]
|
|
|
|
overwrite_denies_speak = (voice_deny.speak is False) or (voice_deny.connect is False)
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
# remove all roles from user that are specified in remove_role_list, only if its a new punish
|
|
|
|
if str(member.id) not in punished:
|
|
|
|
if nitro_role != "no_nitro_role":
|
|
|
|
nitro_role = role_from_string(guild, nitro_role)
|
2020-02-09 11:09:50 +13:00
|
|
|
remove_role_set.discard(nitro_role)
|
|
|
|
|
|
|
|
user_roles = set(member.roles)
|
2020-01-20 20:21:38 +13:00
|
|
|
# build lists of roles that *should* be removed and ones that *can* be
|
|
|
|
removed_roles = user_roles & remove_role_set
|
|
|
|
too_high_to_remove = {r for r in removed_roles if r >= guild.me.top_role}
|
2020-01-28 09:04:50 +13:00
|
|
|
user_roles -= removed_roles - too_high_to_remove
|
2020-01-20 20:21:38 +13:00
|
|
|
user_roles.add(role) # add punish role to the set
|
|
|
|
await member.edit(roles=user_roles, reason=f"punish {member}")
|
|
|
|
|
|
|
|
else:
|
2020-01-28 09:04:50 +13:00
|
|
|
removed_roles = set(resolve_role_list(guild, current.get("removed_roles", [])))
|
2020-01-20 20:21:38 +13:00
|
|
|
too_high_to_remove = {r for r in removed_roles if r >= guild.me.top_role}
|
|
|
|
|
|
|
|
if removed_roles:
|
|
|
|
actually_removed = removed_roles - too_high_to_remove
|
|
|
|
if actually_removed:
|
|
|
|
msg += "\nRemoved roles: {}".format(format_list(*(r.name for r in actually_removed)))
|
|
|
|
|
|
|
|
if too_high_to_remove:
|
|
|
|
fmt_list = format_list(*(r.name for r in removed_roles))
|
2020-01-28 09:04:50 +13:00
|
|
|
msg += "\n" + warning(
|
|
|
|
"These roles were too high to remove (fix hierarchy, then run "
|
|
|
|
"`{}punishset sync-roles`): {}".format(ctx.prefix, fmt_list)
|
|
|
|
)
|
2020-01-20 20:21:38 +13:00
|
|
|
if member.voice:
|
|
|
|
muted = member.voice.mute
|
|
|
|
else:
|
|
|
|
muted = False
|
|
|
|
|
|
|
|
async with self.config.guild(guild).PUNISHED() as punished:
|
|
|
|
punished[str(member.id)] = {
|
2020-01-28 09:04:50 +13:00
|
|
|
"start": current.get("start") or now, # don't override start time if updating
|
|
|
|
"until": until,
|
|
|
|
"by": current.get("by") or ctx.author.id, # don't override original moderator
|
|
|
|
"reason": reason,
|
|
|
|
"unmute": overwrite_denies_speak and not muted,
|
|
|
|
"caseno": case_number,
|
|
|
|
"removed_roles": [r.id for r in removed_roles],
|
2020-01-20 20:21:38 +13:00
|
|
|
}
|
|
|
|
|
|
|
|
if member.voice and overwrite_denies_speak:
|
|
|
|
if member.voice.channel:
|
|
|
|
await member.edit(mute=True)
|
|
|
|
|
|
|
|
# schedule callback for role removal
|
|
|
|
if until:
|
|
|
|
await self.schedule_unpunish(until, member)
|
|
|
|
|
|
|
|
if not quiet:
|
|
|
|
await ctx.send(msg)
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
# Functions related to unpunishing
|
|
|
|
|
|
|
|
async def schedule_unpunish(self, until, member):
|
|
|
|
"""
|
|
|
|
Schedules role removal, canceling and removing existing tasks if present
|
|
|
|
"""
|
|
|
|
|
|
|
|
await self.put_queue_event(until, member.guild.id, member.id)
|
|
|
|
|
|
|
|
def execute_unpunish(self, guild_id, member_id) -> bool:
|
|
|
|
guild = self.bot.get_guild(guild_id)
|
|
|
|
|
|
|
|
if not guild:
|
|
|
|
return False
|
|
|
|
|
|
|
|
member = guild.get_member(member_id)
|
|
|
|
|
|
|
|
if member:
|
2020-03-22 19:04:29 +13:00
|
|
|
asyncio.create_task(self._unpunish(member))
|
2020-01-20 20:21:38 +13:00
|
|
|
return True
|
|
|
|
else:
|
2020-03-22 19:06:43 +13:00
|
|
|
asyncio.create_task(self.bot.request_offline_members(guild))
|
2020-01-20 20:21:38 +13:00
|
|
|
return False
|
|
|
|
|
|
|
|
async def _unpunish(self, member, reason=None, apply_roles=True, update=False, moderator=None, quiet=False) -> bool:
|
|
|
|
"""
|
|
|
|
Remove punish role, delete record and task handle
|
|
|
|
"""
|
|
|
|
guild = member.guild
|
|
|
|
role = await self.get_role(guild, quiet=True)
|
|
|
|
nitro_role = await self.config.guild(guild).NITRO_ID()
|
|
|
|
|
|
|
|
if role:
|
|
|
|
data = await self.config.guild(guild).PUNISHED()
|
|
|
|
member_data = data.get(str(member.id), {})
|
2020-01-28 09:04:50 +13:00
|
|
|
caseno = member_data.get("caseno")
|
|
|
|
removed_roles = set(resolve_role_list(guild, member_data.get("removed_roles", [])))
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
# Has to be done first to prevent triggering listeners
|
|
|
|
await self._unpunish_data(member)
|
|
|
|
await self.cancel_queue_event(member.guild.id, member.id)
|
|
|
|
|
|
|
|
if apply_roles:
|
|
|
|
|
|
|
|
# readd removed roles from user, by replacing user's roles with all of their roles plus the ones that
|
|
|
|
# were removed (and can be re-added), minus the punish role
|
2020-02-09 11:09:50 +13:00
|
|
|
user_roles = set(member.roles)
|
2020-01-20 20:21:38 +13:00
|
|
|
too_high_to_restore = {r for r in removed_roles if r >= guild.me.top_role}
|
|
|
|
removed_roles -= too_high_to_restore
|
|
|
|
user_roles |= removed_roles
|
|
|
|
user_roles.discard(role)
|
|
|
|
await member.edit(roles=user_roles, reason="punish end")
|
|
|
|
|
|
|
|
if update and caseno:
|
2020-01-28 09:04:50 +13:00
|
|
|
until = member_data.get("until") or False
|
2020-01-20 20:21:38 +13:00
|
|
|
# fallback gracefully
|
2020-01-28 09:04:50 +13:00
|
|
|
moderator = moderator or guild.get_member(member_data.get("by")) or guild.me
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
if until:
|
|
|
|
until = datetime.utcfromtimestamp(until).timestamp()
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
edits = {"reason": reason}
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
if moderator.id != data.get("by"):
|
|
|
|
edits["amended_by"] = moderator
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
edits["modified_at"] = time.time()
|
|
|
|
edits["until"] = until
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
try:
|
|
|
|
case = await modlog.get_case(caseno, guild, self.bot)
|
|
|
|
await case.edit(edits)
|
|
|
|
except Exception:
|
|
|
|
pass
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
if member_data.get("unmute", False):
|
2020-01-20 20:21:38 +13:00
|
|
|
if member.voice:
|
|
|
|
if member.voice.channel:
|
|
|
|
await member.edit(mute=False)
|
|
|
|
else:
|
|
|
|
async with self.config.guild(guild).PENDING_UNMUTE() as unmute_list:
|
|
|
|
if member.id not in unmute_list:
|
|
|
|
unmute_list.append(member.id)
|
|
|
|
|
|
|
|
if quiet:
|
|
|
|
return True
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
msg = "Your punishment in %s has ended." % member.guild.name
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
if reason:
|
|
|
|
msg += "\nReason: %s" % reason
|
|
|
|
|
|
|
|
if removed_roles:
|
|
|
|
msg += "\n\nRestored roles: {}.".format(format_list(*(r.name for r in removed_roles)))
|
|
|
|
|
|
|
|
if too_high_to_restore:
|
|
|
|
fmt_list = format_list(*(r.name for r in too_high_to_restore))
|
2020-01-28 09:04:50 +13:00
|
|
|
msg += "\n" + warning(
|
|
|
|
"These roles were too high for me to restore: {}. " "Ask a mod for help.".format(fmt_list)
|
|
|
|
)
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
try:
|
|
|
|
await member.send(msg)
|
|
|
|
return True
|
|
|
|
except Exception:
|
|
|
|
return False
|
|
|
|
|
|
|
|
async def _unpunish_data(self, member):
|
|
|
|
"""Removes punish data entry and cancels any present callback"""
|
|
|
|
guild = member.guild
|
|
|
|
|
|
|
|
async with self.config.guild(guild).PUNISHED() as punished:
|
|
|
|
if str(member.id) in punished:
|
2020-01-28 09:04:50 +13:00
|
|
|
del punished[str(member.id)]
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
# Listeners
|
|
|
|
@commands.Cog.listener()
|
|
|
|
async def on_guild_channel_create(self, channel):
|
|
|
|
"""Run when new channels are created and set up role permissions"""
|
|
|
|
role = await self.get_role(channel.guild, quiet=True)
|
|
|
|
if not role:
|
|
|
|
return
|
|
|
|
|
|
|
|
await self.setup_channel(channel, role)
|
|
|
|
|
|
|
|
@commands.Cog.listener()
|
|
|
|
async def on_member_update(self, before, after):
|
2020-01-27 15:41:06 +13:00
|
|
|
"""Remove scheduled unpunish when manually removed role"""
|
2020-01-20 20:21:38 +13:00
|
|
|
try:
|
2020-01-27 15:41:06 +13:00
|
|
|
assert before.roles != after.roles
|
2020-01-20 20:21:38 +13:00
|
|
|
guild_data = await self.config.guild(before.guild).PUNISHED()
|
|
|
|
member_data = guild_data[str(before.id)]
|
|
|
|
role = await self.get_role(before.guild, quiet=True)
|
|
|
|
assert role
|
|
|
|
except (KeyError, AssertionError):
|
|
|
|
return
|
|
|
|
|
|
|
|
new_roles = {role.id: role for role in after.roles}
|
|
|
|
|
|
|
|
if role in before.roles and role.id not in new_roles:
|
2020-01-28 09:04:50 +13:00
|
|
|
msg = "Punishment manually ended early by a moderator/admin."
|
2020-01-20 20:21:38 +13:00
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
if member_data["reason"]:
|
|
|
|
msg += "\nReason was: " + member_data["reason"]
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
await self._unpunish(after, reason=msg, update=True)
|
|
|
|
else:
|
2020-01-28 09:04:50 +13:00
|
|
|
to_remove = {new_roles.get(role_id) for role_id in member_data.get("removed_roles", [])}
|
2020-01-20 20:21:38 +13:00
|
|
|
to_remove = [r for r in to_remove if r and r < after.guild.me.top_role]
|
|
|
|
|
|
|
|
if to_remove:
|
|
|
|
await after.remove_roles(*to_remove)
|
|
|
|
|
|
|
|
@commands.Cog.listener()
|
|
|
|
async def on_member_join(self, member):
|
|
|
|
"""Restore punishment if punished user leaves/rejoins"""
|
|
|
|
guild = member.guild
|
|
|
|
punished = await self.config.guild(guild).PUNISHED()
|
|
|
|
data = punished.get(str(member.id), {})
|
|
|
|
|
|
|
|
if not data:
|
|
|
|
return
|
|
|
|
|
|
|
|
# give other tools a chance to settle, then re-fetch data just in case
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
member = self.bot.get_guild(guild.id).get_member(member.id)
|
|
|
|
role = await self.get_role(member.guild, quiet=True)
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
until = data["until"]
|
2020-01-20 20:21:38 +13:00
|
|
|
duration = until - time.time()
|
|
|
|
|
|
|
|
if role and duration > 0:
|
|
|
|
await self.schedule_unpunish(until, member)
|
|
|
|
|
|
|
|
if role not in member.roles:
|
|
|
|
await member.add_roles(role)
|
|
|
|
|
|
|
|
@commands.Cog.listener()
|
|
|
|
async def on_voice_state_update(self, member, before, after):
|
|
|
|
if not after.channel:
|
|
|
|
return
|
|
|
|
|
|
|
|
guild = member.guild
|
|
|
|
data = await self.config.guild(guild).PUNISHED()
|
|
|
|
member_data = data.get(str(member.id), {})
|
|
|
|
unmute_list = await self.config.guild(guild).PENDING_UNMUTE()
|
|
|
|
|
|
|
|
if member_data and not after.mute:
|
|
|
|
await member.edit(mute=True)
|
|
|
|
elif member.id in unmute_list:
|
|
|
|
await member.edit(mute=False)
|
|
|
|
if member.id in unmute_list:
|
|
|
|
unmute_list.remove(member.id)
|
|
|
|
|
|
|
|
await self.config.guild(guild).PENDING_UNMUTE.set(unmute_list)
|
|
|
|
|
|
|
|
@commands.Cog.listener()
|
|
|
|
async def on_member_ban(self, member):
|
|
|
|
"""Remove punishment record when member is banned."""
|
|
|
|
guild = member.guild
|
|
|
|
data = await self.config.guild(guild).PUNISHED()
|
|
|
|
member_data = data.get(str(member.id))
|
|
|
|
|
|
|
|
if member_data is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
msg = "Punishment ended early due to ban."
|
|
|
|
|
2020-01-28 09:04:50 +13:00
|
|
|
if member_data.get("reason"):
|
|
|
|
msg += "\n\nOriginal reason was: " + member_data["reason"]
|
2020-01-20 20:21:38 +13:00
|
|
|
|
|
|
|
await self._unpunish(member, reason=msg, apply_roles=False, update=True, quiet=True)
|