async def mute(self, ctx, user: discord.Member, duration: Optional[Duration], *, reason: Reason = None): """Mutes a user using the guild's configured mute role. Sends the user a DM and logs this action to the guild's modlog if configured to do so. """ reason, note, audit_reason = reason or (None, None, None) audit_reason = audit_reason or Reason.format_reason(ctx) dt = exact_timedelta(duration) if duration else "permanent" try: muted_user, has_role = await MutedUser().convert(ctx, str(user.id)) except commands.BadArgument: muted_user, has_role = None, None # if already muted, edit the duration if muted_user and has_role: # first make sure they have an infraction. it's hard to edit an infraction that doesn't exist. if await modlog.has_active_infraction(ctx.guild.id, user.id, 'mute'): i, edited, old = await self._do_mute_duration_edit( guild=ctx.guild, user=user, new_duration=duration, edited_by=ctx.author) old = exact_timedelta(old) if old else 'permanent' await ctx.send(f"{TICK_YELLOW} User is already muted (#{i})" + (f", changed duration instead ({old} -> {dt})." if edited else '.')) # just kidding, we couldn't find an infraction. let's see if they want to create one. # note: we ask for a confirmation so things don't break when two infractions go through simultaneously else: await ctx.confirm_action( f"{TICK_YELLOW} This user appears to have this guild's mute role, " f"but does not have any active mute infractions. " f"Would you like to create an infraction? (y/n)") await LogEvent('mute', ctx.guild, user, ctx.author, reason, note, duration).dispatch() await ctx.send(OK_HAND) # otherwise, mute the user like normal else: delivered = await self._do_mute(guild=ctx.guild, user=user, mod=ctx.author, reason=reason, note=note, audit_reason=audit_reason, duration=duration) await ctx.send( f"{OK_HAND} Muted **{user}** ({dt}). {notified(delivered)}")
async def edit_infraction_and_message(infraction, **kwargs): if 'edited_by' in kwargs: edit = f"(edited by {kwargs.pop('edited_by')})" else: edit = '' k1, k2 = kwargs.copy(), kwargs.copy() if 'duration' in kwargs: duration = kwargs.pop('duration') k1['ends_at'] = infraction.created_at + duration if duration else None d = exact_timedelta(duration) if duration else 'permanent' k2['duration'] = f"{d} {edit}" if 'reason' in kwargs: r = kwargs.pop('reason') k1['reason'] = k2['reason'] = f"{r} {edit}" if 'note' in kwargs: n = kwargs.pop('note') k1['note'] = k2['note'] = f"{n} {edit}" i = await db.edit_record(infraction, **k1) try: m = await edit_log_message(infraction, **k2) except discord.HTTPException as e: raise OuranosCommandError( f"I edited the infraction, but was unable to edit the modlog message " f"({e.text.lower().capitalize()}).") return i, m
async def log_forceban(guild, user, mod, reason, note, duration): infraction = await new_infraction(guild.id, user.id, mod.id, 'ban', reason, note, duration, True) duration = exact_timedelta(duration) if duration else None content = format_log_message(EMOJI_BAN, 'USER FORCEBANNED', infraction.infraction_id, duration, user, mod, reason, note) message = await new_log_message(guild, content) await db.edit_record(infraction, message_id=message.id)
async def log_mute(guild, user, mod, reason, note, duration): infraction = await new_infraction(guild.id, user.id, mod.id, 'mute', reason, note, duration, True) duration = exact_timedelta(duration) if duration else None content = format_log_message(EMOJI_MUTE, 'MEMBER MUTED', infraction.infraction_id, duration, user, mod, reason, note) message = await new_log_message(guild, content) await db.edit_record(infraction, message_id=message.id)
async def log_mass_mute(guild, users, mod, reason, note, duration): infraction_ids = await get_case_id(guild.id, count=len(users)) user_ids = [user.id for user in users] await new_infractions_bulk(guild.id, user_ids, mod.id, 'mute', reason, note, duration, True, infraction_ids) duration = exact_timedelta(duration) if duration else None content = format_mass_action_log_message( EMOJI_MUTE, 'USERS MASS-MUTED', (infraction_ids[0], infraction_ids[-1]), duration, len(users), mod, reason, note) message = await new_log_message(guild, content) await db.Infraction.filter(infraction_id__in=infraction_ids ).update(message_id=message.id)
async def history(self, ctx, *, user: UserID): """Returns useful info on a user's recent infractions.""" history = await self._get_history(ctx.guild.id, user.id) now = time.time() s = "" h_by_type = {} for _type in [ 'note', 'warn', 'mute', 'unmute', 'kick', 'ban', 'unban' ]: h_by_type.update({i: _type for i in getattr(history, _type)}) infraction_ids = sorted(h_by_type.keys(), reverse=True) infractions = [ await modlog.get_infraction(ctx.guild.id, i) for i in infraction_ids ] if infractions: _recent = infractions[:5] s += f"Recent infractions for {user} (showing {len(_recent)}/{len(infractions)}):```\n" for infraction in _recent: mod = await self.bot.get_or_fetch_member( ctx.guild, infraction.mod_id) or infraction.mod_id dt = approximate_timedelta(now - infraction.created_at) dt_tot = exact_timedelta( infraction.ends_at - infraction.created_at) if infraction.ends_at else None dt_rem = approximate_timedelta( infraction.ends_at - now) if infraction.ends_at else None active = 'active ' if infraction.active else '' s += f"#{infraction.infraction_id}: {active}{infraction.type} by {mod} ({dt} ago)\n" if dt_tot: rem = f" ({dt_rem} remaining)" if infraction.active else '' s += f"\tduration: {dt_tot}{rem}\n" if infraction.reason: s += f"\treason: {infraction.reason}\n" s += "```" else: s += f"No infractions for {user}." await ctx.send(s)
async def edit_infractions_and_messages_bulk(infractions, linked=True, **kwargs): """Edits a set of infractions. All must be linked to the same log message.""" inf = infractions[0] for i in infractions: if i.type != inf.type: raise OuranosCommandError( "Infraction type mismatch during bulk edit.") if linked: if i.message_id != inf.message_id: raise OuranosCommandError( "Infraction message_id mismatch during bulk edit.") elif i.created_at != inf.created_at: raise OuranosCommandError( "Infraction created_at mismatch during bulk edit.") if 'edited_by' in kwargs: edit = f"(edited by {kwargs.pop('edited_by')})" else: edit = '' k1, k2 = kwargs.copy(), kwargs.copy() if 'duration' in kwargs: duration = kwargs.pop('duration') k1['ends_at'] = inf.created_at + duration if duration else None d = exact_timedelta(duration) if duration else 'permanent' k2['duration'] = f"{d} {edit}" if 'reason' in kwargs: r = kwargs.pop('reason') k1['reason'] = k2['reason'] = f"{r} {edit}" if 'note' in kwargs: n = kwargs.pop('note') k1['note'] = k2['note'] = f"{n} {edit}" i = await db.edit_records_bulk(infractions, **k1) if linked: try: m = await edit_log_message(inf, **k2) except discord.HTTPException as e: raise OuranosCommandError( f"I edited the infractions, but was unable to edit the modlog message " f"({e.text.lower()}).") return i, m # edit a batch of messages else: count = 0 skip = set() for inf in infractions: if inf.infraction_id in skip: continue # make sure we only edit each message once if inf.bulk_infraction_id_range: start, end = inf.bulk_infraction_id_range for _i_id in range(start, end + 1): skip.add(_i_id) # actually edit now try: await edit_log_message(inf, **k2) count += 1 except discord.HTTPException as e: raise OuranosCommandError( f"I edited the infractions, but was unable to edit the modlog message " f"for #{inf.infraction_id} ({e.text.lower()}).") return i, count
async def ban(self, ctx, user: UserID, duration: Optional[Duration], *, reason: Reason = None): """Bans a user from the guild. This will also work if the user is not in the server. Sends the user a DM and logs this action to the guild's modlog if configured to do so. """ reason, note, audit_reason = reason or (None, None, None) audit_reason = audit_reason or Reason.format_reason(ctx) dt = exact_timedelta(duration) if duration else "permanent" try: banned_user, banned_in_guild = await BannedUser().convert( ctx, str(user.id)) except commands.BadArgument: banned_user, banned_in_guild = None, None # if already banned, edit the duration if banned_user and banned_in_guild: # first make sure they have an infraction. it's hard to edit an infraction that doesn't exist. if await modlog.has_active_infraction(ctx.guild.id, user.id, 'ban'): i, edited, old = await self._do_ban_duration_edit( guild=ctx.guild, user=user, new_duration=duration, edited_by=ctx.author) old = exact_timedelta(old) if old else 'permanent' await ctx.send(f"{TICK_YELLOW} User is already banned (#{i})" + (f", changed duration instead ({old} -> {dt})." if edited else '.')) # just kidding, we couldn't find an infraction. let's see if they want to create one. # note: we ask for a confirmation so things don't break when two infractions go through simultaneously else: await ctx.confirm_action( f"{TICK_YELLOW} This user appears to be banned from this guild, " f"but does not have any active ban infractions. " f"Would you like to create an infraction? (y/n)") await LogEvent('ban', ctx.guild, user, ctx.author, reason, note, duration).dispatch() await ctx.send(OK_HAND) # we didn't seem to find anything weird, so let's just ban! else: user, delivered, force = await self._do_ban( guild=ctx.guild, user=user, mod=ctx.author, reason=reason, note=note, audit_reason=audit_reason, duration=duration) banned = 'Forcebanned' if force else 'Banned' await ctx.send( f"{HAMMER} {banned} **{user}** ({dt}). {notified(delivered)}")
async def _do_mass_ban(self, ctx, users, mod, reason, note, audit_reason, duration=None): """Bans a set of users and dispatches the event to the modlog.""" guild = ctx.guild users = set(users) total = len(users) if total <= 1: raise ModerationError("Not enough users to ban.") await ctx.confirm_action( f"{TICK_YELLOW} Are you sure you would like to ban {total} users? (y/n)" ) # filter out already-banned users ban_list = set(b.user.id for b in await guild.bans()) bannable_users = [u for u in users if u.id not in ban_list] already_banned = total - len(bannable_users) # check to make sure we can actually do this for user in users: member = await self.bot.get_or_fetch_member(guild, user.id) if not guild.me.guild_permissions.ban_members: raise BotMissingPermission('Ban Members') if member: if not guild.me.top_role > member.top_role: raise BotRoleHierarchyError if await is_server_mod(member): raise ModActionOnMod m = await ctx.send("Banning...") # do the actual bans now success = [] not_found = 0 for user in bannable_users: try: await guild.ban(user, reason=audit_reason, delete_message_days=0) success.append(user) except discord.NotFound: not_found += 1 # dispatch the modlog event if success: await MassActionLogEvent('ban', guild, success, mod, reason, note, duration).dispatch() # format response message def _s(i): return "s" if i != 1 else "" content = f"{ZAP} Banned {len(success)}/{total} users" if success: dt = exact_timedelta(duration) if duration else 'permanent' content += f' ({dt}).' else: content += '.' extra_lines = [ f"{already_banned} user{_s(already_banned)} already banned" if already_banned else "", f"{not_found} user{_s(not_found)} not found" if not_found else "" ] extra = ", ".join(e for e in extra_lines if e) if extra: content += " *" + extra + ".*" await m.edit(content=content)
async def _do_mass_mute(self, ctx, users, mod, reason, note, audit_reason, duration=None): """Mutes a set of users and dispatches the event to the modlog.""" guild = ctx.guild users = set(users) total = len(users) config = await db.get_config(guild) role = guild.get_role(config.mute_role_id if config else 0) if total <= 1: raise ModerationError("Not enough users to mute.") await ctx.confirm_action( f"{TICK_YELLOW} Are you sure you would like to mute {total} users? (y/n)" ) # check to make sure we can actually do this not_in_guild = 0 already_muted = 0 mutable_members = [] for user in users: member = await self.bot.get_or_fetch_member(guild, user.id) if not member: not_in_guild += 1 continue elif role in member.roles: already_muted += 1 continue else: mutable_members.append(member) if not role: raise NotConfigured('mute_role') if not guild.me.guild_permissions.manage_roles: raise BotMissingPermission('Manage Roles') if not guild.me.top_role > role: raise BotRoleHierarchyError if await is_server_mod(member): raise ModActionOnMod m = await ctx.send("Muting...") # do the actual mutes now success = [] for member in mutable_members: await member.add_roles(role, reason=audit_reason) success.append(member) # dispatch the modlog event if success: await MassActionLogEvent('mute', guild, success, mod, reason, note, duration).dispatch() # format response message def _s(i): return "s" if i != 1 else "" content = f"{OK_HAND} Muted {len(success)}/{total} users" if success: dt = exact_timedelta(duration) if duration else 'permanent' content += f' ({dt}).' else: content += '.' extra_lines = [ f"{already_muted} user{_s(already_muted)} already muted" if already_muted else "", f"{not_in_guild} user{_s(not_in_guild)} not found" if not_in_guild else "" ] extra = ", ".join(e for e in extra_lines if e) if extra: content += " *" + extra + ".*" await m.edit(content=content)