2020-01-20 20:21:38 +13:00
# redbot/discord
2022-06-15 18:07:13 +12:00
from concurrent . futures import thread
2020-01-20 20:21:38 +13:00
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
2022-05-31 09:44:54 +12:00
from . discord_thread_feature import add_user_thread , create_thread
2020-01-20 20:21:38 +13:00
# general
import asyncio
from datetime import datetime
2021-02-06 21:40:00 +13:00
from typing import Literal
2020-01-20 20:21:38 +13:00
import inspect
import logging
import time
import textwrap
2022-05-31 09:44:54 +12:00
from typing import Union
2020-01-20 20:21:38 +13:00
2020-01-28 09:04:50 +13:00
log = logging . getLogger ( " red.punish " )
2020-01-20 20:21:38 +13:00
2020-10-29 21:36:43 +13:00
__version__ = " 3.3.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 ,
2022-05-31 09:44:54 +12:00
" use_threads " : False ,
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.1 h10m 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 ( ) :
2020-07-29 05:18:33 +12:00
intid = int ( mid )
if guild . get_member ( intid ) :
2020-01-20 20:21:38 +13:00
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 ( )
2020-07-29 05:18:33 +12:00
@checks.bot_has_permissions ( ban_members = True )
2020-01-20 20:21:38 +13:00
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 ( )
2020-07-29 05:18:33 +12:00
bans = await guild . bans ( )
ban_ids = { ban . user . id for ban in bans }
2020-01-20 20:21:38 +13:00
for mid , mdata in data . copy ( ) . items ( ) :
2020-07-29 05:18:33 +12:00
intid = int ( mid )
if guild . get_member ( intid ) :
2020-01-20 20:21:38 +13:00
continue
2020-07-29 05:18:33 +12:00
elif intid 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 \n Reason 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 \n Original reason was: " + data [ " reason " ]
2020-01-20 20:21:38 +13:00
2022-06-15 18:07:13 +12:00
if data . get ( " thread " , None ) is not None :
msg + = f " \n \n <# { data [ ' thread ' ] } > "
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 + = " \n Restored 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 ,
2021-02-06 21:42:47 +13:00
" **Moderator**: %s "
% ( user . guild . get_member ( int ( data . get ( " by " ) ) ) or " Missing ID# %s " % data . get ( " by " ) ) ,
2020-01-28 09:04:50 +13:00
]
)
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 ` %s reason` 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 + = " \n Mod 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
2022-05-31 09:44:54 +12:00
@punishset.command ( name = " threads " )
async def punishset_threads ( self , ctx , use_threads : bool ) :
"""
Have punished users put into their own threads when punished
These threads are private and will be seen by moderators with the Manage Threads permission and the punished user .
"""
if " PRIVATE_THREADS " not in ctx . guild . features and use_threads :
await ctx . send (
error ( " Your guild must be boosted to Level 2 to use private threads, which this feature requires. " )
)
return
2020-01-20 20:21:38 +13:00
2022-05-31 09:44:54 +12:00
await self . config . guild ( ctx . guild ) . use_threads . set ( use_threads )
await ctx . tick ( )
2020-01-20 20:21:38 +13:00
2022-05-31 09:44:54 +12:00
@punishset.command ( name = " remove-roles " )
async def punishset_remove_role_list ( self , ctx , * rolelist : Union [ discord . Role , str ] ) :
""" Set what roles to remove when punishing.
2020-01-20 20:21:38 +13:00
2022-05-31 09:44:54 +12:00
List of roles can be mentions , names , or role IDs . Role name with spaces must be put in quotes
2020-01-20 20:21:38 +13:00
"""
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 )
2022-05-31 09:44:54 +12:00
if not rolelist :
2020-01-20 20:21:38 +13:00
if current_roles :
names_list = format_list ( * ( r . name for r in current_roles ) )
2022-05-31 09:44:54 +12:00
await ctx . send ( f " Current list of roles removed when a user is punished: ` { names_list } ` " )
2020-01-20 20:21:38 +13:00
else :
await ctx . send ( " No roles defined for removal. " )
return
2022-05-31 09:44:54 +12:00
elif " role_list_clear " in rolelist :
2020-01-20 20:21:38 +13:00
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 )
2022-05-31 09:44:54 +12:00
for lookup in rolelist :
if not isinstance ( lookup , str ) :
found_roles . add ( lookup )
continue
2020-01-20 20:21:38 +13:00
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 \n Please try again. " ) )
elif punish_role and punish_role in found_roles :
await ctx . send ( warning ( " The punished role cannot be removed. \n \n Please try again. " ) )
elif guild . default_role in found_roles :
await ctx . send ( warning ( " The everyone role cannot be removed. \n \n Please try again. " ) )
elif found_roles == set ( current_roles ) :
await ctx . send ( " No changes to make. " )
else :
if punished :
extra = f " \n \n Run ` { 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 ) )
2022-05-31 09:44:54 +12:00
await ctx . send ( f " Will remove these roles when a user is punished: ` { fmt_list } . { extra } ` " )
2020-01-20 20:21:38 +13:00
@punishset.command ( name = " nitro-role " )
2022-05-31 09:44:54 +12:00
async def punishset_nitro_role ( self , ctx , * , role : Union [ discord . Role , str ] = None ) :
2020-01-20 20:21:38 +13:00
"""
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 )
2022-05-31 09:44:54 +12:00
if role and isinstance ( role , str ) and role . lower ( ) == " no_nitro_role " :
2020-01-20 20:21:38 +13:00
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
2022-05-31 09:44:54 +12:00
if isinstance ( role , str ) :
role = role_from_string ( guild , role )
2020-01-20 20:21:38 +13:00
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 ( ) :
2021-02-06 20:49:21 +13:00
member = guild . get_member ( int ( member_id ) )
2020-01-20 20:21:38 +13:00
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 ` %s punishset 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 ` %s punishset 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 \n Run ` %s punishset 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 ` %s punishset 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 + " \n configuring 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 + " \n 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 )
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 ( )
2021-02-06 20:49:21 +13:00
_guilds = [ g for g in self . bot . guilds if g . large and not ( g . chunked or g . unavailable ) ]
await self . bot . request_offline_members ( * _guilds )
2020-01-20 20:21:38 +13:00
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 " ]
2021-02-06 20:49:21 +13:00
member = guild . get_member ( int ( member_id ) )
2020-01-20 20:21:38 +13:00
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
2022-06-15 18:07:13 +12:00
if data . get ( " thread " , None ) is not None :
reason + = f " \n \n <# { data [ ' thread ' ] } > "
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 :
2021-02-06 20:49:21 +13:00
await self . execute_queue_event ( 0 , * args )
2020-01-20 20:21:38 +13:00
elif run_at - time . time ( ) < QUEUE_TIME_CUTOFF :
2021-02-06 20:49:21 +13:00
self . pending [ args ] = asyncio . create_task ( self . execute_queue_event ( diff , * 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 :
2021-02-06 20:49:21 +13:00
if await self . execute_queue_event ( 0 , * args ) :
2020-01-20 20:21:38 +13:00
return
elif diff < QUEUE_TIME_CUTOFF :
2021-02-06 20:49:21 +13:00
self . pending [ args ] = asyncio . create_task ( self . execute_queue_event ( diff , * args ) )
2020-01-20 20:21:38 +13:00
return True
await self . queue . put ( item )
return False
2021-02-06 20:49:21 +13:00
async def execute_queue_event ( self , diff , * args ) - > bool :
await asyncio . sleep ( diff )
2020-01-20 20:21:38 +13:00
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
2022-06-15 18:07:13 +12:00
case = None
2020-01-20 20:21:38 +13:00
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 ( )
2022-05-31 09:44:54 +12:00
use_threads = await self . config . guild ( guild ) . use_threads ( )
2020-01-20 20:21:38 +13:00
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-11-06 21:40:11 +13:00
# check if user is isolated, fix conflict with isolate cog
isolate = self . bot . get_cog ( " Isolate " )
if isolate :
isolated = await isolate . config . guild ( guild ) . ISOLATED ( )
if str ( member . id ) in isolated :
await ctx . send (
warning ( " This person is isolated, I will remove it now before punishing to avoid conflicts. " )
)
await ctx . invoke ( isolate . isolate_end , user = member , reason = " Conflict with punish cog. " )
# double check it actually worked
isolated = await isolate . config . guild ( guild ) . ISOLATED ( )
if str ( member . id ) in isolated :
2022-05-31 09:44:54 +12:00
await ctx . send ( error ( " Couldn ' t remove isolation from user, please do so manually. " ) )
2020-11-06 21:40:11 +13:00
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. \n Try 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 :
2022-06-15 18:07:13 +12: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 + = " \n Removed 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
2022-06-15 18:07:13 +12:00
if use_threads :
# create thread for user to talk in, if this is a new case
thread_name = f " Case { case_number } - { member . name } "
thread_name = thread_name [ : 101 ] # 100 character name limit
channel = await self . config . guild ( guild ) . CHANNEL_ID ( )
channel = guild . get_channel ( channel )
thread_id = None
if channel is None :
await ctx . send ( error ( " Punish channel not found! " ) )
else :
try :
thread_id = await create_thread ( self . bot , channel , thread_name , archive = 10080 )
# add punished user to thread
await add_user_thread ( self . bot , thread_id , member )
# add moderator who sanctioned the action to the thread
await add_user_thread ( self . bot , thread_id , ctx . author )
except AttributeError :
await ctx . send (
error (
" Your guild no longer has Level 2 boost, private threads and punish threads will not function. "
)
)
except Exception as e :
await ctx . send (
error (
f " I could not create the thread for the user, please make sure I have the `Manage Threads` permission on the punish channel and the punish role is setup correctly (punished user can view the punish channel)! \n \n { e } "
)
)
# modify case to include mention to thread channel for easy access
if thread_id is not None :
try :
edits = { " reason " : reason + f " \n \n <# { thread_id } > " }
await case . edit ( edits )
except Exception as e :
await ctx . send ( warning ( f " Couldn ' t edit case to add thread mention: { e } " ) )
2020-01-20 20:21:38 +13:00
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
}
2022-06-15 18:07:13 +12:00
if use_threads and thread_id is not None :
punished [ str ( member . id ) ] [ " thread " ] = thread_id
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
2021-02-06 20:49:21 +13:00
member = guild . get_member ( int ( member_id ) )
2020-01-20 20:21:38 +13:00
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
2021-02-06 20:49:21 +13:00
moderator = moderator or guild . get_member ( int ( 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 + = " \n Reason: %s " % reason
if removed_roles :
msg + = " \n \n Restored 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 ) :
2020-10-29 21:36:43 +13:00
if await self . bot . cog_disabled_in_guild ( self , channel . guild ) :
return
2020-01-20 20:21:38 +13:00
""" 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-10-29 21:36:43 +13:00
if await self . bot . cog_disabled_in_guild ( self , after . guild ) :
return
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 + = " \n Reason 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 """
2020-10-29 21:36:43 +13:00
if await self . bot . cog_disabled_in_guild ( self , member . guild ) :
return
2020-01-20 20:21:38 +13:00
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 ) :
2020-10-29 22:31:38 +13:00
if await self . bot . cog_disabled_in_guild ( self , member . guild ) :
2020-10-29 21:36:43 +13:00
return
2020-01-20 20:21:38 +13:00
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. """
2020-10-29 21:36:43 +13:00
if await self . bot . cog_disabled_in_guild ( self , member . guild ) :
return
2020-01-20 20:21:38 +13:00
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 \n Original 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 )
2021-02-06 21:40:00 +13:00
async def red_delete_data_for_user (
self ,
* ,
requester : Literal [ " discord_deleted_user " , " owner " , " user " , " user_strict " ] ,
user_id : int ,
) :
pass