async def on_member_ban(self, guild: discord.Guild, member: discord.Member) -> None: """Log ban event to user log.""" if guild.id != GuildConstant.id: return if member.id in self._ignored[Event.member_ban]: self._ignored[Event.member_ban].remove(member.id) return await self.send_log_message( Icons.user_ban, Colours.soft_red, "User banned", format_user(member), thumbnail=member.avatar_url_as(static_format="png"), channel_id=Channels.user_log)
async def upload_messages(self, actor_id: int, modlog: ModLog) -> None: """Method that takes care of uploading the queue and posting modlog alert.""" triggered_by_users = ", ".join(format_user(m) for m in self.members) triggered_in_channel = f"**Triggered in:** {self.triggered_in.mention}\n" if len( self.channels) > 1 else "" channels_description = ", ".join(channel.mention for channel in self.channels) mod_alert_message = ( f"**Triggered by:** {triggered_by_users}\n" f"{triggered_in_channel}" f"**Channels:** {channels_description}\n" f"**Rules:** {', '.join(rule for rule in self.rules)}\n") messages_as_list = list(self.messages.values()) first_message = messages_as_list[0] # For multiple messages and those with attachments or excessive newlines, use the logs API if any((len(messages_as_list) > 1, len(first_message.attachments) > 0, first_message.content.count('\n') > 15)): url = await modlog.upload_log(self.messages.values(), actor_id, self.attachments) mod_alert_message += f"A complete log of the offending messages can be found [here]({url})" else: mod_alert_message += "Message:\n" content = first_message.clean_content remaining_chars = 4080 - len(mod_alert_message) if len(content) > remaining_chars: url = await modlog.upload_log([first_message], actor_id, self.attachments) log_site_msg = f"The full message can be found [here]({url})" content = content[:remaining_chars - (3 + len(log_site_msg))] + "..." mod_alert_message += content await modlog.send_log_message( icon_url=Icons.filtering, colour=Colour(Colours.soft_red), title="Spam detected!", text=mod_alert_message, thumbnail=first_message.author.display_avatar.url, channel_id=Channels.mod_alerts, ping_everyone=AntiSpamConfig.ping_everyone)
async def on_member_join(self, member: discord.Member) -> None: """Log member join event to user log.""" if member.guild.id != GuildConstant.id: return now = datetime.utcnow() difference = abs(relativedelta(now, member.created_at)) message = format_user(member) + "\n\n**Account age:** " + humanize_delta(difference) if difference.days < 1 and difference.months < 1 and difference.years < 1: # New user account! message = f"{Emojis.new} {message}" await self.send_log_message( Icons.sign_in, Colours.soft_green, "User joined", message, thumbnail=member.avatar_url_as(static_format="png"), channel_id=Channels.user_log )
async def pardon_voice_ban(self, user_id: int, guild: discord.Guild, reason: t.Optional[str]) -> t.Dict[str, str]: """Add Voice Verified role back to user, DM them a notification, and return a log dict.""" user = guild.get_member(user_id) log_text = {} if user: # DM user about infraction expiration notified = await _utils.notify_pardon( user=user, title="Voice ban ended", content="You have been unbanned and can verify yourself again in the server.", icon_url=_utils.INFRACTION_ICONS["voice_ban"][1] ) log_text["Member"] = format_user(user) log_text["DM"] = "Sent" if notified else "**Failed**" else: log_text["Info"] = "User was not found in the guild." return log_text
async def upload_messages(self, actor_id: int, modlog: ModLog) -> None: """Method that takes care of uploading the queue and posting modlog alert.""" triggered_by_users = ", ".join(format_user(m) for m in self.members) triggered_in_channel = f"**Triggered in:** {self.triggered_in.mention}\n" if len(self.channels) > 1 else "" channels_description = ", ".join(channel.mention for channel in self.channels) mod_alert_message = ( f"**Triggered by:** {triggered_by_users}\n" f"{triggered_in_channel}" f"**Channels:** {channels_description}\n" f"**Rules:** {', '.join(rule for rule in self.rules)}\n" ) # For multiple messages or those with excessive newlines, use the logs API if len(self.messages) > 1 or 'newlines' in self.rules: url = await modlog.upload_log(self.messages.values(), actor_id, self.attachments) mod_alert_message += f"A complete log of the offending messages can be found [here]({url})" else: mod_alert_message += "Message:\n" [message] = self.messages.values() content = message.clean_content remaining_chars = 4080 - len(mod_alert_message) if len(content) > remaining_chars: content = content[:remaining_chars] + "..." mod_alert_message += f"{content}" *_, last_message = self.messages.values() await modlog.send_log_message( icon_url=Icons.filtering, colour=Colour(Colours.soft_red), title="Spam detected!", text=mod_alert_message, thumbnail=last_message.author.avatar_url_as(static_format="png"), channel_id=Channels.mod_alerts, ping_everyone=AntiSpamConfig.ping_everyone )
async def pardon_infraction(self, ctx: Context, infr_type: str, user: UserSnowflake, send_msg: bool = True) -> None: """ Prematurely end an infraction for a user and log the action in the mod log. If `send_msg` is True, then a pardoning confirmation message will be sent to the context channel. Otherwise, no such message will be sent. """ log.trace(f"Pardoning {infr_type} infraction for {user}.") # Check the current active infraction log.trace(f"Fetching active {infr_type} infractions for {user}.") response = await self.bot.api_client.get('bot/infractions', params={ 'active': 'true', 'type': infr_type, 'user__id': user.id }) if not response: log.debug(f"No active {infr_type} infraction found for {user}.") await ctx.send( f":x: There's no active {infr_type} infraction for user {user.mention}." ) return # Deactivate the infraction and cancel its scheduled expiration task. log_text = await self.deactivate_infraction(response[0], send_log=False) log_text["Member"] = messages.format_user(user) log_text["Actor"] = ctx.author.mention log_content = None id_ = response[0]['id'] footer = f"ID: {id_}" # Accordingly display whether the user was successfully notified via DM. dm_emoji = "" if log_text.get("DM") == "Sent": dm_emoji = ":incoming_envelope: " elif "DM" in log_text: dm_emoji = f"{constants.Emojis.failmail} " # Accordingly display whether the pardon failed. if "Failure" in log_text: confirm_msg = ":x: failed to pardon" log_title = "pardon failed" log_content = ctx.author.mention log.warning( f"Failed to pardon {infr_type} infraction #{id_} for {user}.") else: confirm_msg = ":ok_hand: pardoned" log_title = "pardoned" log.info(f"Pardoned {infr_type} infraction #{id_} for {user}.") # Send a confirmation message to the invoking context. if send_msg: log.trace( f"Sending infraction #{id_} pardon confirmation message.") await ctx.send( f"{dm_emoji}{confirm_msg} infraction **{' '.join(infr_type.split('_'))}** for {user.mention}. " f"{log_text.get('Failure', '')}") # Move reason to end of entry to avoid cutting out some keys log_text["Reason"] = log_text.pop("Reason") # Send a log message to the mod log. await self.mod_log.send_log_message( icon_url=_utils.INFRACTION_ICONS[infr_type][1], colour=Colours.soft_green, title=f"Infraction {log_title}: {' '.join(infr_type.split('_'))}", thumbnail=user.avatar_url_as(static_format="png"), text="\n".join(f"{k}: {v}" for k, v in log_text.items()), footer=footer, content=log_content, )
async def infraction_edit( self, ctx: Context, infraction: Infraction, duration: t.Union[Expiry, allowed_strings("p", "permanent"), None], # noqa: F821 *, reason: str = None ) -> None: """ Edit the duration and/or the reason of an infraction. Durations are relative to the time of updating and should be appended with a unit of time. Units (∗case-sensitive): \u2003`y` - years \u2003`m` - months∗ \u2003`w` - weeks \u2003`d` - days \u2003`h` - hours \u2003`M` - minutes∗ \u2003`s` - seconds Use "l", "last", or "recent" as the infraction ID to specify that the most recent infraction authored by the command invoker should be edited. Use "p" or "permanent" to mark the infraction as permanent. Alternatively, an ISO 8601 timestamp can be provided for the duration. """ if duration is None and reason is None: # Unlike UserInputError, the error handler will show a specified message for BadArgument raise commands.BadArgument("Neither a new expiry nor a new reason was specified.") infraction_id = infraction["id"] request_data = {} confirm_messages = [] log_text = "" if duration is not None and not infraction['active']: if reason is None: await ctx.send(":x: Cannot edit the expiration of an expired infraction.") return confirm_messages.append("expiry unchanged (infraction already expired)") elif isinstance(duration, str): request_data['expires_at'] = None confirm_messages.append("marked as permanent") elif duration is not None: request_data['expires_at'] = duration.isoformat() expiry = time.format_infraction_with_duration(request_data['expires_at']) confirm_messages.append(f"set to expire on {expiry}") else: confirm_messages.append("expiry unchanged") if reason: request_data['reason'] = reason confirm_messages.append("set a new reason") log_text += f""" Previous reason: {infraction['reason']} New reason: {reason} """.rstrip() else: confirm_messages.append("reason unchanged") # Update the infraction new_infraction = await self.bot.api_client.patch( f'bot/infractions/{infraction_id}', json=request_data, ) # Re-schedule infraction if the expiration has been updated if 'expires_at' in request_data: # A scheduled task should only exist if the old infraction wasn't permanent if infraction['expires_at']: self.infractions_cog.scheduler.cancel(new_infraction['id']) # If the infraction was not marked as permanent, schedule a new expiration task if request_data['expires_at']: self.infractions_cog.schedule_expiration(new_infraction) log_text += f""" Previous expiry: {infraction['expires_at'] or "Permanent"} New expiry: {new_infraction['expires_at'] or "Permanent"} """.rstrip() changes = ' & '.join(confirm_messages) await ctx.send(f":ok_hand: Updated infraction #{infraction_id}: {changes}") # Get information about the infraction's user user_id = new_infraction['user'] user = ctx.guild.get_member(user_id) if user: user_text = messages.format_user(user) thumbnail = user.avatar_url_as(static_format="png") else: user_text = f"<@{user_id}>" thumbnail = None await self.mod_log.send_log_message( icon_url=constants.Icons.pencil, colour=discord.Colour.blurple(), title="Infraction edited", thumbnail=thumbnail, text=textwrap.dedent(f""" Member: {user_text} Actor: <@{new_infraction['actor']}> Edited by: {ctx.message.author.mention}{log_text} """) )
log_text += f""" Previous expiry: {until_expiration(infraction['expires_at']) or "Permanent"} New expiry: {until_expiration(new_infraction['expires_at']) or "Permanent"} """.rstrip() changes = ' & '.join(confirm_messages) await ctx.send( f":ok_hand: Updated infraction #{infraction_id}: {changes}") # Get information about the infraction's user user_id = new_infraction['user'] user = await get_or_fetch_member(ctx.guild, user_id) if user: user_text = messages.format_user(user) thumbnail = user.display_avatar.url else: user_text = f"<@{user_id}>" thumbnail = None await self.mod_log.send_log_message(icon_url=constants.Icons.pencil, colour=discord.Colour.og_blurple(), title="Infraction edited", thumbnail=thumbnail, text=textwrap.dedent(f""" Member: {user_text} Actor: <@{new_infraction['actor']}> Edited by: {ctx.message.author.mention}{log_text} """), footer=f"ID: {infraction_id}")
def format_log_message(msg: Message, token: str) -> str: """Return the generic portion of the log message to send for `token` being censored in `msg`.""" return LOG_MESSAGE.format(author=format_user(msg.author), channel=msg.channel.mention, token=token)