def test_positive_action_warn(): """This test also covers Note, Mute, and Ban""" discord_id, warn_text = randint(10, 10000), "this is a test warning" logged_action = models.Action(mod=BASE_USER, server=BASE_SERVER) new_warn = models.Warn( text=warn_text, user=models.User(discord_id=discord_id), server=BASE_SERVER, action=logged_action, ) SESSION.add(new_warn) SESSION.commit() user = SESSION.query( models.User).filter(models.User.discord_id == discord_id).one() assert new_warn in user.warn warn = SESSION.query(models.Warn).filter(models.Warn.user == user).first() assert warn.text == warn_text assert warn.action == logged_action
async def add(self, ctx, user_id: str, *, note_text: str): """Adds a note to a users record. Example: note add userID this is a test note note add @wumpus#0000 this is a test note Requires Permission: Manage Messages Parameters ----------- ctx: context The context message involved. user_id: str The user/member the notes are related to. Can be an ID or a mention note_text: str The note text you are adding to the record. """ session = self.bot.helpers.get_db_session() try: self.bot.log.info( f"CMD {ctx.command} called by {ctx.message.author} ({ctx.message.author.id})" ) user = await self.bot.helpers.get_member_or_user( user_id, ctx.message.guild) if not user: return await ctx.send( f"Unable to find the requested user. Please make sure the user ID or @ mention is valid." ) # Get mod's DB profile db_mod = await self.bot.helpers.db_get_user( session, ctx.message.author.id) # Get the DB profile for the guild db_guild = await self.bot.helpers.db_get_guild( session, ctx.message.guild.id) # Get the DB profile for the user db_user = await self.bot.helpers.db_get_user(session, user.id) logged_action = models.Action(mod=db_mod, server=db_guild) new_note = models.Note(text=note_text, user=db_user, server=db_guild, action=logged_action) session.add(new_note) session.commit() await ctx.send( f"Successfully stored note #{new_note.id} for: {user} ({user.id})" ) except discord.HTTPException as err: self.bot.log.error( f"Discord HTTP Error responding to {ctx.command} request via Msg ID {ctx.message.id}. {sys.exc_info()[0].__name__}: {err}" ) await ctx.send( f"Error processing {ctx.command}. Error has already been reported to my developers." ) except DBAPIError as err: self.bot.log.exception( f"Error logging note to database. {sys.exc_info()[0].__name__}: {err}" ) await ctx.send( f"Unable to log note due to database error for: ({user_id}). Error has already been reported to my developers." ) session.rollback() except Exception as err: self.bot.log.exception( f"Error responding to {ctx.command} via Msg ID {ctx.message.id}. {sys.exc_info()[0].__name__}: {err}" ) await ctx.send( f"Error processing {ctx.command}. Error has already been reported to my developers." ) finally: session.close()
async def edit(self, ctx, user_id: str, note_id: str, *, note_text: str): """Edits a note on a users record. Example: note edit userID noteID new note message note add @wumpus#0000 new note message Requires Permission: Manage Messages Parameters ----------- ctx: context The context message involved. user_id: str The user/member the note is related to. note_id: str The note id to edit. note_text: str The note text you are updating on the record. """ session = self.bot.helpers.get_db_session() try: user = await self.bot.helpers.get_member_or_user( user_id, ctx.message.guild) if not user: return await ctx.send( f"Unable to find the requested user. Please make sure the user ID or @ mention is valid." ) # Clean the Note ID since some people put the #numb when it should just be int try: note_id = int(note_id.replace("#", "")) except ValueError: return await ctx.send( f"You must provide the Note ID that you want to edit.") # Get mod's DB profile db_mod = await self.bot.helpers.db_get_user( session, ctx.message.author.id) # Get the DB profile for the guild db_guild = await self.bot.helpers.db_get_guild( session, ctx.message.guild.id) # Get the DB profile for the user db_user = await self.bot.helpers.db_get_user(session, user.id) # Get the note by the ID logged_note = session.query(models.Note).get(note_id) # Now let's make sure the user isn't trying to update a note they aren't authorized for if logged_note and (logged_note.server_id == db_guild.id and logged_note.user_id == db_user.id): new_action = models.Action(mod=db_mod, server=db_guild) session.add(new_action) session.commit() logged_note.text = note_text logged_note.action_id = new_action.id session.add(logged_note) session.commit() await ctx.send( f"Successfully updated note #{logged_note.id} for: {user} ({user.id})" ) else: await ctx.send( f"Unable to update that note. Please make sure you are providing a valid note ID and user ID." ) except discord.HTTPException as err: self.bot.log.error( f"Discord HTTP Error responding to {ctx.command} request via Msg ID {ctx.message.id}. {sys.exc_info()[0].__name__}: {err}" ) await ctx.send( f"Error processing {ctx.command}. Error has already been reported to my developers." ) except DBAPIError as err: self.bot.log.exception( f"Error saving edited note for: ({user_id}). {sys.exc_info()[0].__name__}: {err}" ) await ctx.send( f"Error processing {ctx.command}. Error has already been reported to my developers." ) session.rollback() except Exception as err: self.bot.log.exception( f"Error responding to {ctx.command} via Msg ID {ctx.message.id}. {sys.exc_info()[0].__name__}: {err}" ) await ctx.send( f"Error processing {ctx.command}. Error has already been reported to my developers." ) finally: session.close()
async def mute( self, ctx, user_id: str, mute_time: time.UserFriendlyTime(commands.clean_content, default="\u2026"), *, reason: str, ): """Mute a user. If no time or note specified default values will be used. Time can be a human readable string, many formats are understood. To unmute someone see unmute Requires Permission: Manage Messages Parameters ----------- ctx: context The context message involved. user_id: str The Discord ID or user mention the command is being run on. mute_time: time How long the mute will be for. reason: str The reason for the mute. This will be sent to the user and added to the logs. """ self.bot.log.info( f"CMD {ctx.command} called by {ctx.message.author} ({ctx.message.author.id})" ) # If we were provided an ID, let's try and use it if user_id: member = await self.bot.helpers.get_member_or_user( user_id, ctx.message.guild) if not member: return await ctx.send( f"Unable to find the requested user. Please make sure the user ID or @ mention is valid." ) elif isinstance(member, discord.User): await ctx.send( f"The user specified does not appear to be in the server. Proceeding with mute in case they return." ) else: return await ctx.send( f"A user ID or Mention must be provided for who to mute.") if mute_time == "20m": mute_time = datetime.datetime.now( datetime.timezone.utc) + datetime.timedelta(minutes=20) elif isinstance(mute_time, datetime.datetime): mute_time = mute_time else: mute_time = mute_time.dt mute_length_human = time.human_timedelta(mute_time) settings = self.bot.guild_settings.get(ctx.message.guild.id) has_modmail_server = settings.modmail_server_id muted_role_id = settings.muted_role mod_channel = discord.utils.get(ctx.message.guild.text_channels, id=settings.mod_channel) if not mod_channel: await ctx.send( "Please set a mod channel using `config modchannel #channel`") # delete the message we used to invoke it if mod_channel and ctx.message.channel.id != mod_channel.id: try: await ctx.message.delete() except discord.HTTPException as err: self.bot.log.warning( f"Couldn't delete command message for {ctx.command}: {err}" ) log_channel = discord.utils.get(ctx.message.guild.text_channels, name="bot-logs") if not log_channel: # If there is no normal logs channel, try the sweeper (legacy) logs channel log_channel = discord.utils.get(ctx.message.guild.text_channels, name="sweeper-logs") if not log_channel: return await ctx.send( f"No log channel setup. Please create a channel called #bot-logs" ) muted_role = ctx.message.guild.get_role(muted_role_id) if not muted_role: return await ctx.send( "Mute role is not yet configured. Unable to proceed.") footer_text = (self.bot.constants.footer_with_modmail.format( guild=ctx.message.guild) if has_modmail_server else self.bot.constants.footer_no_modmail.format( guild=ctx.message.guild)) sweeper_emoji = self.bot.get_emoji( self.bot.constants.reactions["animated_sweeperbot"]) session = self.bot.helpers.get_db_session() try: old_mute_len = None old_mute_dt = None if not isinstance(member, discord.User): if muted_role in member.roles: if (ctx.message.guild.id in self.current_mutes and member.id in self.current_mutes[ctx.message.guild.id]): old_mute_len = self.current_mutes[ ctx.message.guild.id][member.id].human_delta old_mute_dt = self.current_mutes[ctx.message.guild.id][ member.id].expires self.current_mutes[ctx.message.guild.id][ member.id].stop() del self.current_mutes[ctx.message.guild.id][member.id] if (member is ctx.message.guild.owner or member.bot or member is ctx.message.author): return await ctx.send( "You may not use this command on that user.") if member.top_role > ctx.me.top_role: return await ctx.send( "The user has higher permissions than the bot, can't use this command on that user." ) actionMsg = await ctx.send("Initiating action. Please wait.") self.bot.log.info( f"Initiating mute for user: {member} ({member.id}) in guild {ctx.message.guild} ({ctx.message.guild.id})" ) old_roles = [] old_roles_snow = [] if not isinstance(member, discord.User): for role in member.roles: if role.managed or role.name == "@everyone": continue else: old_roles_snow.append(role) old_roles.append(role.id) # Remove all non-managed roles await member.remove_roles( *old_roles_snow, reason= f"Muted by request of {ctx.message.author} ({ctx.message.author.id})", atomic=True, ) # Assign mute role await member.add_roles( muted_role, reason= f"Muted by request of {ctx.message.author} ({ctx.message.author.id})", atomic=True, ) # If in voice, kick try: if member.voice and member.voice.channel: await member.move_to( channel=None, reason= f"Muted by request of {ctx.message.author.mention}", ) except discord.errors.Forbidden: await ctx.send( f"Missing permissions to drop user from voice channel." ) self.bot.log.info( f"Muted user: {member} ({member.id}) in guild {ctx.message.guild} ({ctx.message.guild.id}) for {mute_length_human}" ) informed_user = False try: # Format the message text = self.bot.constants.infraction_header.format( action_type="mute", guild=ctx.message.guild) # Reduces the text to 1,800 characters to leave enough buffer for header and footer text text += f"This mute is for **{mute_length_human}** with the reason:\n\n" text += reason[:1800] text += footer_text await member.send(text) self.bot.log.info( f"Informed user of their mute: {member} ({member.id}) in guild {ctx.message.guild}" ) informed_user = True if mod_channel and actionMsg.channel.id == mod_channel.id: await actionMsg.edit( content= f"Mute successful for {member.mention}. **Time:** *{mute_length_human}*. {sweeper_emoji}" ) if old_mute_len: await ctx.send( f"**Note**: This user was previously muted until {old_mute_len}." ) else: await actionMsg.edit( content=f"That action was successful. {sweeper_emoji}") except Exception as e: if mod_channel: await mod_channel.send( f"Mute successful for {member.mention}. **Time:** *{mute_length_human}*. {sweeper_emoji}\n" f"However, user couldn't be informed: {e}") if not (type(e) == discord.errors.Forbidden and e.code == 50007): self.bot.log.exception( f"There was an error while informing {member} ({member.id}) about their mute" ) if informed_user: reason += "| **Msg Delivered: Yes**" else: reason += "| **Msg Delivered: No**" # Log action await log.user_action( self.bot, log_channel.name, member, "Mute", f"**Length:** {mute_length_human}\n" f"**Reason:** {reason}", ctx.message.author, ctx.message.guild, ) # Get the DB profile for the guild db_guild = await self.bot.helpers.db_get_guild( session, ctx.message.guild.id) # Get the DB profile for the user db_user = await self.bot.helpers.db_get_user(session, member.id) # Get mod's DB profile db_mod = await self.bot.helpers.db_get_user( session, ctx.message.author.id) db_action = models.Action(mod=db_mod, server=db_guild) db_mute = None if old_mute_len: db_mute = (session.query( models.Mute).filter(models.Mute.server == db_guild).filter( models.Mute.user == db_user).filter( models.Mute.expires == old_mute_dt).one_or_none()) if db_mute: session.add(db_action) session.commit() db_mute.action_id = db_action.id db_mute.text = reason db_mute.expires = mute_time db_mute.updated = datetime.datetime.now(datetime.timezone.utc) else: db_mute = models.Mute( text=reason, user=db_user, server=db_guild, action=db_action, expires=mute_time, old_roles=old_roles, ) session.add(db_mute) session.commit() # Add timer to remove mute timer = Timer.temporary( ctx.message.guild.id, member.id, old_roles, event=self._unmute, expires=mute_time, created=datetime.datetime.now(datetime.timezone.utc), ) timer.start(self.bot.loop) if ctx.message.guild.id not in self.current_mutes: self.current_mutes[ctx.message.guild.id] = {} self.current_mutes[ctx.message.guild.id][member.id] = timer except Exception as e: set_sentry_scope(ctx) if mod_channel: await mod_channel.send( f"There was an error while creating mute for {member.mention}\n" f"**Error**: {e}") self.bot.log.exception( f"There was an error while creating mute for {member} ({member.id})" ) finally: session.close()
async def unmute(self, ctx, member: discord.Member): """Removes a mute for specified user. To Mute someone see mute """ self.bot.log.info( f"CMD {ctx.command} called by {ctx.message.author} ({ctx.message.author.id})" ) settings = self.bot.guild_settings.get(ctx.message.guild.id) muted_role_id = settings.muted_role muted_role = member.guild.get_role(muted_role_id) mod_channel = discord.utils.get(ctx.message.guild.text_channels, id=settings.mod_channel) if not mod_channel: await ctx.send( "Please set a mod channel using `config modchannel #channel`") # delete the message we used to invoke it if mod_channel and ctx.message.channel.id != mod_channel.id: try: await ctx.message.delete() except discord.HTTPException as err: self.bot.log.warning( f"Couldn't delete command message for {ctx.command}: {err}" ) session = self.bot.helpers.get_db_session() try: if muted_role is None: return await ctx.send("Mute role is not yet configured.") if muted_role not in member.roles: return await ctx.send("User is not muted") if (member is member.guild.owner or member.bot or member is ctx.message.author): return await ctx.send( "You may not use this command on that user.") old_mute_dt = None old_roles = [] if (member.guild.id in self.current_mutes and member.id in self.current_mutes[member.guild.id]): old_mute_dt = self.current_mutes[member.guild.id][ member.id].expires query = (session.query(models.Mute.old_roles).filter( models.Mute.expires == old_mute_dt).first()) if query: old_roles = query.old_roles if self._unmute(member.guild.id, member.id, old_roles, ctx.message.author): if ctx.message.channel.id == mod_channel.id: await ctx.send(f"Successfully unmuted {member.mention}.") else: await ctx.send(f"That action was successful.") else: await mod_channel.send( f"Successfully unmuted {member.mention}. However, user could not be informed." ) if old_mute_dt: # Get the DB profile for the guild db_guild = await self.bot.helpers.db_get_guild( session, ctx.message.guild.id) # Get the DB profile for the user db_user = await self.bot.helpers.db_get_user( session, member.id) # Get mod's DB profile db_mod = await self.bot.helpers.db_get_user( session, ctx.message.author.id) db_action = models.Action(mod=db_mod, server=db_guild) db_mute = (session.query( models.Mute).filter(models.Mute.server == db_guild).filter( models.Mute.user == db_user).filter( models.Mute.expires == old_mute_dt).one_or_none()) if db_mute: session.add(db_action) session.commit() db_mute.action_id = db_action.id db_mute.expires = datetime.datetime.now( datetime.timezone.utc) db_mute.updated = datetime.datetime.now( datetime.timezone.utc) session.add(db_mute) session.commit() else: self.bot.log.warning( f"Couldn't find mute for {member} ({member.id}) in database" ) else: self.bot.log.warning( f"Couldn't find mute for {member} ({member.id}) in currently active mutes" ) except Exception as e: set_sentry_scope(ctx) self.bot.log.exception( f"There was an error while unmuting {member} ({member.id})") await mod_channel.send( f"There was an error while unmuting {member.mention}\n" f"**Error**: {e}") finally: session.close()
async def process_unban(self, session, member, mod, guild, action_timestamp, action_text="None"): db_logged = False chan_logged = False try: # Try and log to the database new_note = None try: # Get mod's DB profile db_mod = await self.bot.helpers.db_get_user(session, mod.id) # Get the DB profile for the guild db_guild = await self.bot.helpers.db_get_guild( session, guild.id) # Get the DB profile for the user db_user = await self.bot.helpers.db_get_user( session, member.id) logged_action = models.Action(mod=db_mod, server=db_guild) new_note = models.Note( text=f"(Unban) {action_text}", user=db_user, server=db_guild, action=logged_action, ) session.add(new_note) session.commit() db_logged = True except DBAPIError as err: self.bot.log.exception( f"Error logging unban to database for: ({member}). {sys.exc_info()[0].__name__}: {err}" ) session.rollback() db_logged = False # Create the embed of info description = (f"**Member:** {member} ({member.id})\n" f"**Moderator:** {mod} ({mod.id})\n" f"**Reason:** {action_text[:1900]}") embed = discord.Embed( color=0xBDBDBD, timestamp=action_timestamp, title= f"A users ban was removed | *#{new_note.id if new_note else 'n/a'}*", description=description, ) embed.set_author(name=f"{member} ({member.id})", icon_url=member.avatar_url) # Try and get the logs channel logs = discord.utils.get(guild.text_channels, name="bot-logs") if not logs: # If there is no normal logs channel, try the sweeper (legacy) logs channel logs = discord.utils.get(guild.text_channels, name="sweeper-logs") if logs: # Checks if the bot can even send messages in that channel if (logs.permissions_for(logs.guild.me).send_messages and logs.permissions_for(logs.guild.me).embed_links): await logs.send(embed=embed) chan_logged = True except Exception as err: self.bot.log.exception( f"Error processing ban for user: '******' in Guild: '{guild.id}'. {sys.exc_info()[0].__name__}: {err}" ) raise finally: return db_logged, chan_logged
async def kick(self, ctx, user_id: str, *, action_text: str): """Adds a database record for the user, attempts to message the user about the kick, then kicks from the guild. Example: kick userID this is a test message kick @wumpus#0000 this is a test message k @wumpus#0000 this is a test message Requires Permission: Manage Messages Parameters ----------- ctx: context The context message involved. user_id: str The user/member the action is related to. Can be an ID or a mention action_text: str The action text you are adding to the record. """ session = self.bot.helpers.get_db_session() try: self.bot.log.info( f"CMD {ctx.command} called by {ctx.message.author} ({ctx.message.author.id})" ) # Get the user profile user = await self.bot.helpers.get_member_or_user( user_id, ctx.message.guild) if not user: return await ctx.send( f"Unable to find the requested user. Please make sure the user ID or @ mention is valid." ) # Don't allow you to kick yourself or the guild owner, or itself or other bots. if (user.id in [ ctx.message.author.id, ctx.message.guild.owner.id, self.bot.user.id ] or user.bot): return await ctx.send( f"Sorry, but you are not allowed to do that action to that user." ) # Set some meta data action_type = "Kick" guild = ctx.message.guild settings = self.bot.guild_settings.get(guild.id) modmail_enabled = settings.modmail_server_id # Confirm the action confirm = await self.bot.prompt.send( ctx, f"Are you sure you want to kick {user} ({user.id})?") if confirm is False or None: return await ctx.send("Aborting kicking that user.") elif confirm: # Try to message the user try: # Format the message message = self.bot.constants.infraction_header.format( action_type=action_type, guild=guild) # Reduces the text to 1,800 characters to leave enough buffer for header and footer text message += f"'{action_text[:1800]}'" # Set footer based on if the server has modmail or not if modmail_enabled: message += self.bot.constants.footer_with_modmail.format( guild=guild) else: message += self.bot.constants.footer_no_modmail.format( guild=guild) await user.send(message) user_informed = ( f"User was successfully informed of their {action_type}." ) msg_success = True except discord.errors.Forbidden as err: self.bot.log.warning( f"Error sending {action_type} to user. Bot is either blocked by user or doesn't share a server. Error: {sys.exc_info()[0].__name__}: {err}" ) user_informed = f"User was unable to be informed of their {action_type}. They might not share a server with the bot, their DM's might not allow messages, or they blocked the bot." msg_success = False # Try and log to the database new_kick = None try: # Get mod's DB profile db_mod = await self.bot.helpers.db_get_user( session, ctx.message.author.id) # Get the DB profile for the guild db_guild = await self.bot.helpers.db_get_guild( session, guild.id) # Get the DB profile for the user db_user = await self.bot.helpers.db_get_user( session, user.id) # Log the action to the database # Edit the action_text to indicate success or failure on informing the user. if msg_success: action_text += " | **Msg Delivered: Yes**" else: action_text += " | **Msg Delivered: No**" logged_action = models.Action(mod=db_mod, server=db_guild) new_kick = models.Kick( text=action_text, user=db_user, server=db_guild, action=logged_action, ) session.add(new_kick) session.commit() db_logged = True except Exception as err: self.bot.log.exception( f"Error logging {action_type} to database.") db_logged = False # Create the embed of info description = ( f"**Member:** {user} ({user.id})\n" f"**Moderator:** {ctx.message.author} ({ctx.message.author.id})\n" f"**Reason:** {action_text[:1900]}") embed = discord.Embed( color=0x0083FF, timestamp=datetime.utcnow(), title=f"A user was kicked | *#{new_kick.id}*", description=description, ) embed.set_author(name=f"{user} ({user.id})", icon_url=user.avatar_url) # Try and get the logs channel logs = discord.utils.get(guild.text_channels, name="bot-logs") if not logs: # If there is no normal logs channel, try the sweeper (legacy) logs channel logs = discord.utils.get(guild.text_channels, name="sweeper-logs") if logs: # Checks if the bot can even send messages in that channel if (logs.permissions_for(logs.guild.me).send_messages and logs.permissions_for(logs.guild.me).embed_links): await logs.send(embed=embed) # Now that we've handled messaging the user, let's kick them try: if isinstance(user, discord.member.Member): reason_text = f"Mod: {ctx.message.author} ({ctx.message.author.id}) | Reason: {action_text[:400]}" await guild.kick(user, reason=reason_text) if db_logged: response = f"A {action_type} was successfully logged and actioned for: {user} ({user.id}).\n\n{user_informed}" else: response = f"A {action_type} was unable to be logged, however it was successfully actioned for: {user} ({user.id}).\n\n{user_informed}" await ctx.send(response) else: raise Exception( "User is not in the guild, unable to kick them.") except Exception as err: self.bot.log.warning( f"Failed to kick user. Error: {sys.exc_info()[0].__name__}: {err}" ) await ctx.send( f"Successfully logged a {action_type} for: {user} ({user.id}), however **unable to kick them.** This could mean they weren't in the server.\n\n{user_informed}" ) except discord.HTTPException as err: self.bot.log.error( f"Discord HTTP Error responding to {ctx.command} request via Msg ID {ctx.message.id}. {sys.exc_info()[0].__name__}: {err}" ) await ctx.send( f"Error processing {ctx.command}. Error has already been reported to my developers." ) except DBAPIError as err: self.bot.log.exception( f"Error with database calls in CMD {ctx.command} for: ({user_id}). {sys.exc_info()[0].__name__}: {err}" ) await ctx.send( f"Error processing {ctx.command}. Error has already been reported to my developers." ) session.rollback() except Exception as err: self.bot.log.exception( f"Error responding to {ctx.command} via Msg ID {ctx.message.id}. {sys.exc_info()[0].__name__}: {err}" ) await ctx.send( f"Error processing {ctx.command}. Error has already been reported to my developers." ) finally: session.close()