import asyncio import datetime import discord import logging import random from typing import Optional, Union, Literal from redbot.core import Config, checks, commands from redbot.core.utils.chat_formatting import box, pagify, humanize_list from dateutil.tz import tzlocal from .enums import WhisperType from .errors import WhisperError __author__ = "tmerc" log = logging.getLogger("red.tmerc.welcome") ENABLED = "enabled" DISABLED = "disabled" class Welcome(commands.Cog): """Announce when users join or leave a server.""" default_join = "Welcome {member.mention} to {server.name}!" default_leave = "{member.name} has left {server.name}!" default_ban = "{member.name} has been banned from {server.name}!" default_unban = "{member.name} has been unbanned from {server.name}!" default_whisper = "Hey there {member.name}, welcome to {server.name}!" guild_defaults = { "enabled": False, "channel": None, "date": None, "join": { "enabled": True, "channel": None, "delete": False, "last": None, "counter": 0, "whisper": {"state": "off", "message": default_whisper}, "messages": [default_join], "bot": None, }, "leave": { "enabled": True, "channel": None, "delete": False, "last": None, "counter": 0, "messages": [default_leave], }, "ban": { "enabled": True, "channel": None, "delete": False, "last": None, "counter": 0, "messages": [default_ban], }, "unban": { "enabled": True, "channel": None, "delete": False, "last": None, "counter": 0, "messages": [default_unban], }, } def __init__(self, *args, **kwargs) -> None: super().__init__() self.bot = kwargs["bot"] self.config = Config.get_conf(self, 86345009) self.config.register_guild(**self.guild_defaults) @commands.group(aliases=["welcome"]) @commands.guild_only() @checks.admin_or_permissions(manage_guild=True) async def welcomeset(self, ctx: commands.Context) -> None: """Change Welcome settings.""" await ctx.trigger_typing() if ctx.invoked_subcommand is None: guild: discord.Guild = ctx.guild c = await self.config.guild(guild).all() channel = await self.__get_channel(guild, "default") join_channel = await self.__get_channel(guild, "join") leave_channel = await self.__get_channel(guild, "leave") ban_channel = await self.__get_channel(guild, "ban") unban_channel = await self.__get_channel(guild, "unban") j = c["join"] jw = j["whisper"] v = c["leave"] b = c["ban"] u = c["unban"] whisper_message = jw["message"] if len(jw["message"]) <= 50 else jw["message"][:50] + "..." if await ctx.embed_requested(): emb = discord.Embed(color=await ctx.embed_color(), title="Current Welcome Settings") emb.add_field( name="General", inline=False, value=f"**Enabled:** {c['enabled']}\n**Channel:** {channel.mention}\n", ) emb.add_field( name="Join", inline=False, value=( f"**Enabled:** {j['enabled']}\n" f"**Channel:** {join_channel.mention}\n" f"**Delete previous:** {j['delete']}\n" f"**Whisper state:** {jw['state']}\n" f"**Whisper message:** {whisper_message}\n" f"**Messages:** {len(j['messages'])}; do `{ctx.prefix}welcomeset join msg list` for a list\n" f"**Bot message:** {j['bot']}" ), ) emb.add_field( name="Leave", inline=False, value=( f"**Enabled:** {v['enabled']}\n" f"**Channel:** {leave_channel.mention}\n" f"**Delete previous:** {v['delete']}\n" f"**Messages:** {len(v['messages'])}; do `{ctx.prefix}welcomeset leave msg list` for a list\n" ), ) emb.add_field( name="Ban", inline=False, value=( f"**Enabled:** {b['enabled']}\n" f"**Channel:** {ban_channel.mention}\n" f"**Delete previous:** {b['delete']}\n" f"**Messages:** {len(b['messages'])}; do `{ctx.prefix}welcomeset ban msg list` for a list\n" ), ) emb.add_field( name="Unban", inline=False, value=( f"**Enabled:** {u['enabled']}\n" f"**Channel:** {unban_channel.mention}\n" f"**Delete previous:** {u['delete']}\n" f"**Messages:** {len(u['messages'])}; do `{ctx.prefix}welcomeset unban msg list` for a list\n" ), ) await ctx.send(embed=emb) else: msg = box( f" Enabled: {c['enabled']}\n" f" Channel: {channel}\n" f" Join:\n" f" Enabled: {j['enabled']}\n" f" Channel: {join_channel}\n" f" Delete previous: {j['delete']}\n" f" Whisper:\n" f" State: {jw['state']}\n" f" Message: {whisper_message}\n" f" Messages: {len(j['messages'])}; do '{ctx.prefix}welcomeset join msg list' for a list\n" f" Bot message: {j['bot']}\n" f" Leave:\n" f" Enabled: {v['enabled']}\n" f" Channel: {leave_channel}\n" f" Delete previous: {v['delete']}\n" f" Messages: {len(v['messages'])}; do '{ctx.prefix}welcomeset leave msg list' for a list\n" f" Ban:\n" f" Enabled: {b['enabled']}\n" f" Channel: {ban_channel}\n" f" Delete previous: {b['delete']}\n" f" Messages: {len(b['messages'])}; do '{ctx.prefix}welcomeset ban msg list' for a list\n" f" Unban:\n" f" Enabled: {u['enabled']}\n" f" Channel: {unban_channel}\n" f" Delete previous: {u['delete']}\n" f" Messages: {len(u['messages'])}; do '{ctx.prefix}welcomeset unban msg list' for a list\n", "Current Welcome Settings", ) await ctx.send(msg) @welcomeset.command(name="toggle") async def welcomeset_toggle(self, ctx: commands.Context, on_off: bool = None) -> None: """Turns Welcome on or off. If `on_off` is not provided, the state will be flipped. """ guild = ctx.guild target_state = on_off if on_off is not None else not (await self.config.guild(guild).enabled()) await self.config.guild(guild).enabled.set(target_state) await ctx.send(f"Welcome is now {ENABLED if target_state else DISABLED}.") @welcomeset.command(name="channel") async def welcomeset_channel(self, ctx: commands.Context, channel: discord.TextChannel) -> None: """Sets the channel to be used for event notices.""" if not Welcome.__can_speak_in(channel): await ctx.send( f"I do not have permission to send messages in {channel.mention}. " "Check your permission settings and try again." ) return guild = ctx.guild await self.config.guild(guild).channel.set(channel.id) await ctx.send(f"I will now send event notices to {channel.mention}.") @welcomeset.group(name="join") async def welcomeset_join(self, ctx: commands.Context) -> None: """Change settings for join notices.""" pass @welcomeset_join.command(name="toggle") async def welcomeset_join_toggle(self, ctx: commands.Context, on_off: bool = None) -> None: """Turns join notices on or off. If `on_off` is not provided, the state will be flipped. """ await self.__toggle(ctx, on_off, "join") @welcomeset_join.command(name="channel") async def welcomeset_join_channel(self, ctx: commands.Context, channel: discord.TextChannel = None) -> None: """Sets the channel to be used specifically for join notices. If `channel` is not provided, the join-specific channel is cleared. """ await self.__set_channel(ctx, channel, "join") @welcomeset_join.command(name="toggledelete") async def welcomeset_join_toggledelete(self, ctx: commands.Context, on_off: bool = None) -> None: """Turns deletion of previous join notice on or off. If `on_off` is not provided, the state will be flipped. """ await self.__toggledelete(ctx, on_off, "join") @welcomeset_join.group(name="whisper") async def welcomeset_join_whisper(self, ctx: commands.Context) -> None: """Change settings for join whispers.""" pass @welcomeset_join_whisper.command(name="type") async def welcomeset_join_whisper_type(self, ctx: commands.Context, choice: WhisperType) -> None: """Set if a DM is sent to the new member. Options: off - no DM is sent only - only send a DM to the member, do not send a message to the channel both - send a DM to the member and a message to the channel fall - send a DM to the member, if it fails send the whisper message to the channel instead """ guild = ctx.guild whisper_type = choice.value channel = await self.__get_channel(ctx.guild, "join") await self.config.guild(guild).join.whisper.state.set(whisper_type) if choice == WhisperType.OFF: await ctx.send(f"I will no longer DM new members, and will send a notice to {channel.mention}.") elif choice == WhisperType.ONLY: await ctx.send(f"I will now only DM new members, and will not send a notice to {channel.mention}.") elif choice == WhisperType.BOTH: await ctx.send(f"I will now send a DM to new members, as well as send a notice to {channel.mention}.") elif choice == WhisperType.FALLBACK: await ctx.send( f"I will now send a DM to new members, and if that fails I will send the message to {channel.mention}." ) @welcomeset_join_whisper.command(name="message", aliases=["msg"]) async def welcomeset_join_whisper_message(self, ctx: commands.Context, *, msg_format: str) -> None: """Set the message DM'd to new members when they join. Allows for the following customizations: `{member}` is the member who joined `{server}` is the server """ await self.config.guild(ctx.guild).join.whisper.message.set(msg_format) await ctx.send("I will now use that message format when whispering new members, if whisper is enabled.") @welcomeset_join.group(name="message", aliases=["msg"]) async def welcomeset_join_message(self, ctx: commands.Context) -> None: """Manage join message formats.""" pass @welcomeset_join_message.command(name="add") async def welcomeset_join_message_add(self, ctx: commands.Context, *, msg_format: str) -> None: """Add a new join message format to be chosen. Allows for the following customizations: `{member}` is the new member `{server}` is the server `{count}` is the number of members who have joined today `{plural}` is an 's' if `count` is not 1, and nothing if it is `{stats}` to include user stats (if using activitylog cog) `{roles}` to show member roles at time of event For example: {member.mention}... What are you doing here??? {server.name} has a new member! {member.name}#{member.discriminator} - {member.id} Someone new has joined! Who is it?! D: IS HE HERE TO HURT US?! """ await self.__message_add(ctx, msg_format, "join") @welcomeset_join_message.command(name="delete", aliases=["del"]) async def welcomeset_join_message_delete(self, ctx: commands.Context) -> None: """Delete an existing join message format from the list.""" await self.__message_delete(ctx, "join") @welcomeset_join_message.command(name="list", aliases=["ls"]) async def welcomeset_join_message_list(self, ctx: commands.Context) -> None: """Lists the available join message formats.""" await self.__message_list(ctx, "join") @welcomeset_join.command(name="botmessage", aliases=["botmsg"]) async def welcomeset_join_botmessage(self, ctx: commands.Context, *, msg_format: str = None) -> None: """Sets the message format to use for join notices for bots. Supply no format to use normal join message formats for bots. Allows for the following customizations: `{bot}` is the bot `{server}` is the server `{count}` is the number of members who have joined today `{plural}` is an 's' if `count` is not 1, and nothing if it is For example: {bot.mention} beep boop. """ await self.config.guild(ctx.guild).join.bot.set(msg_format) if msg_format is not None: await ctx.send("Bot join message format set. I will now greet bots with that message.") else: await ctx.send("Bot join message format removed. I will now greet bots like normal members.") @welcomeset.group(name="leave") async def welcomeset_leave(self, ctx: commands.Context) -> None: """Change settings for leave notices.""" pass @welcomeset_leave.command(name="toggle") async def welcomeset_leave_toggle(self, ctx: commands.Context, on_off: bool = None) -> None: """Turns leave notices on or off. If `on_off` is not provided, the state will be flipped. """ await self.__toggle(ctx, on_off, "leave") @welcomeset_leave.command(name="channel") async def welcomeset_leave_channel(self, ctx: commands.Context, channel: discord.TextChannel = None) -> None: """Sets the channel to be used specifically for leave notices. If `channel` is not provided, the leave-specific channel is cleared. """ await self.__set_channel(ctx, channel, "leave") @welcomeset_leave.command(name="toggledelete") async def welcomeset_leave_toggledelete(self, ctx: commands.Context, on_off: bool = None) -> None: """Turns deletion of previous leave notice on or off. If `on_off` is not provided, the state will be flipped. """ await self.__toggledelete(ctx, on_off, "leave") @welcomeset_leave.group(name="message", aliases=["msg"]) async def welcomeset_leave_message(self, ctx: commands.Context) -> None: """Manage leave message formats.""" pass @welcomeset_leave_message.command(name="add") async def welcomeset_leave_message_add(self, ctx: commands.Context, *, msg_format: str) -> None: """Add a new leave message format to be chosen. Allows for the following customizations: `{member}` is the member who left `{server}` is the server `{count}` is the number of members who have left today `{plural}` is an 's' if `count` is not 1, and nothing if it is `{stats}` to include user stats (if using activitylog cog) `{roles}` to show member roles at time of event `{joined_on}` is the Discord formated time the user joined the server For example: {member.name}... Why did you leave??? {server.name} has lost a member! {member.name}#{member.discriminator} - {member.id} Someone has left... Aww... Bye :( """ await self.__message_add(ctx, msg_format, "leave") @welcomeset_leave_message.command(name="delete", aliases=["del"]) async def welcomeset_leave_message_delete(self, ctx: commands.Context) -> None: """Delete an existing leave message format from the list.""" await self.__message_delete(ctx, "leave") @welcomeset_leave_message.command(name="list", aliases=["ls"]) async def welcomeset_leave_message_list(self, ctx: commands.Context) -> None: """Lists the available leave message formats.""" await self.__message_list(ctx, "leave") @welcomeset.group(name="ban") async def welcomeset_ban(self, ctx: commands.Context) -> None: """Change settings for ban notices.""" pass @welcomeset_ban.command(name="toggle") async def welcomeset_ban_toggle(self, ctx: commands.Context, on_off: bool = None) -> None: """Turns ban notices on or off. If `on_off` is not provided, the state will be flipped. """ await self.__toggle(ctx, on_off, "ban") @welcomeset_ban.command(name="channel") async def welcomeset_ban_channel(self, ctx: commands.Context, channel: discord.TextChannel = None) -> None: """Sets the channel to be used specifically for ban notices. If `channel` is not provided, the ban-specific channel is cleared. """ await self.__set_channel(ctx, channel, "ban") @welcomeset_ban.command(name="toggledelete") async def welcomeset_ban_toggledelete(self, ctx: commands.Context, on_off: bool = None) -> None: """Turns deletion of previous ban notice on or off. If `on_off` is not provided, the state will be flipped. """ await self.__toggledelete(ctx, on_off, "ban") @welcomeset_ban.group(name="message", aliases=["msg"]) async def welcomeset_ban_message(self, ctx: commands.Context) -> None: """Manage ban message formats.""" pass @welcomeset_ban_message.command(name="add") async def welcomeset_ban_message_add(self, ctx: commands.Context, *, msg_format: str) -> None: """Add a new ban message format to be chosen. Allows for the following customizations: `{member}` is the banned member `{server}` is the server `{count}` is the number of members who have been banned today `{plural}` is an 's' if `count` is not 1, and nothing if it is `{stats}` to include user stats (if using activitylog cog) `{roles}` to show member roles at time of event For example: {member.name} was banned... What did you do??? A member of {server.name} has been banned! {member.name}#{member.discriminator} - {member.id} Someone has been banned. Good riddance! """ await self.__message_add(ctx, msg_format, "ban") @welcomeset_ban_message.command(name="delete", aliases=["del"]) async def welcomeset_ban_message_delete(self, ctx: commands.Context) -> None: """Delete an existing ban message format from the list.""" await self.__message_delete(ctx, "ban") @welcomeset_ban_message.command(name="list", aliases=["ls"]) async def welcomeset_ban_message_list(self, ctx: commands.Context) -> None: """Lists the available ban message formats.""" await self.__message_list(ctx, "ban") @welcomeset.group(name="unban") async def welcomeset_unban(self, ctx: commands.Context) -> None: """Change settings for unban notices.""" pass @welcomeset_unban.command(name="toggle") async def welcomeset_unban_toggle(self, ctx: commands.Context, on_off: bool = None) -> None: """Turns unban notices on or off. If `on_off` is not provided, the state will be flipped. """ await self.__toggle(ctx, on_off, "unban") @welcomeset_unban.command(name="channel") async def welcomeset_unban_channel(self, ctx: commands.Context, channel: discord.TextChannel = None) -> None: """Sets the channel to be used specifically for unban notices. If `channel` is not provided, the unban-specific channel is cleared. """ await self.__set_channel(ctx, channel, "unban") @welcomeset_unban.command(name="toggledelete") async def welcomeset_unban_toggledelete(self, ctx: commands.Context, on_off: bool = None) -> None: """Turns deletion of previous unban notice on or off. If `on_off` is not provided, the state will be flipped. """ await self.__toggledelete(ctx, on_off, "unban") @welcomeset_unban.group(name="message", aliases=["msg"]) async def welcomeset_unban_message(self, ctx: commands.Context) -> None: """Manage unban message formats.""" pass @welcomeset_unban_message.command(name="add") async def welcomeset_unban_message_add(self, ctx: commands.Context, *, msg_format: str) -> None: """Add a new unban message format to be chosen. Allows for the following customizations: `{member}` is the unbanned member `{server}` is the server `{count}` is the number of members who have been unbanned today `{plural}` is an 's' if `count` is not 1, and nothing if it is For example: {member.name} was unbanned... Did you learn your lesson??? A member of {server.name} has been unbanned! {member.name}#{member.discriminator} - {member.id} Someone has been unbanned. Don't waste your second chance! """ await self.__message_add(ctx, msg_format, "unban") @welcomeset_unban_message.command(name="delete", aliases=["del"]) async def welcomeset_unban_message_delete(self, ctx: commands.Context) -> None: """Delete an existing unban message format from the list.""" await self.__message_delete(ctx, "unban") @welcomeset_unban_message.command(name="list", aliases=["ls"]) async def welcomeset_unban_message_list(self, ctx: commands.Context) -> None: """Lists the available unban message formats.""" await self.__message_list(ctx, "unban") @commands.Cog.listener() async def on_member_join(self, member: discord.Member) -> None: """Listens for member joins.""" if await self.bot.cog_disabled_in_guild(self, member.guild): return guild: discord.Guild = member.guild guild_settings = self.config.guild(guild) if await guild_settings.enabled() and await guild_settings.join.enabled(): # join notice should be sent message_format: Optional[str] = None if member.bot: # bot message_format = await guild_settings.join.bot() else: whisper_type: str = await guild_settings.join.whisper.state() if whisper_type != "off": try: await self.__dm_user(member) except WhisperError: if whisper_type == "fall": message_format = await self.config.guild(member.guild).join.whisper.message() await self.__handle_event(guild, member, "join", message_format=message_format) return if whisper_type == "only" or whisper_type == "fall": # we're done here return await self.__handle_event(guild, member, "join", message_format=message_format) @commands.Cog.listener() async def on_member_remove(self, member: discord.Member) -> None: """Listens for member leaves.""" if await self.bot.cog_disabled_in_guild(self, member.guild): return await self.__handle_event(member.guild, member, "leave") @commands.Cog.listener() async def on_member_ban(self, guild: discord.Guild, member: discord.User) -> None: """Listens for user bans.""" await self.__handle_event(guild, member, "ban") @commands.Cog.listener() async def on_member_unban(self, guild: discord.Guild, user: discord.User) -> None: """Listens for user unbans.""" await self.__handle_event(guild, user, "unban") # # concrete handlers for settings changes and events # async def __toggle(self, ctx: commands.Context, on_off: bool, event: str) -> None: """Handler for setting toggles.""" guild: discord.Guild = ctx.guild target_state = on_off if on_off is not None else not (await self.config.guild(guild).get_attr(event).enabled()) await self.config.guild(guild).get_attr(event).enabled.set(target_state) await ctx.send(f"{event.capitalize()} notices are now {ENABLED if target_state else DISABLED}.") async def __set_channel(self, ctx: commands.Context, channel: discord.TextChannel, event: str) -> None: """Handler for setting channels.""" guild: discord.Guild = ctx.guild store_this = channel.id if channel is not None else None await self.config.guild(guild).get_attr(event).channel.set(store_this) if store_this is not None: await ctx.send(f"I will now send {event} notices to {channel.mention}.") else: default_channel = await self.__get_channel(guild, "default") await ctx.send(f"I will now send {event} messages to the default channel, {default_channel.mention}.") async def __toggledelete(self, ctx: commands.Context, on_off: bool, event: str) -> None: """Handler for setting delete toggles.""" guild: discord.Guild = ctx.guild target_state = on_off if on_off is not None else not (await self.config.guild(guild).get_attr(event).delete()) await self.config.guild(guild).get_attr(event).delete.set(target_state) await ctx.send(f"Deletion of previous {event} notice is now {ENABLED if target_state else DISABLED}") async def __message_add(self, ctx: commands.Context, msg_format: str, event: str) -> None: """Handler for adding message formats.""" guild: discord.Guild = ctx.guild async with self.config.guild(guild).get_attr(event).messages() as messages: messages.append(msg_format) await ctx.send(f"New message format for {event} notices added.") async def __message_delete(self, ctx: commands.Context, event: str) -> None: """Handler for deleting message formats.""" guild: discord.Guild = ctx.guild async with self.config.guild(guild).get_attr(event).messages() as messages: if len(messages) == 1: await ctx.send(f"I only have one {event} message format, so I can't let you delete it.") return await self.__message_list(ctx, event) await ctx.send(f"Please enter the number of the {event} message format you wish to delete.") try: num = await Welcome.__get_number_input(ctx, len(messages)) except asyncio.TimeoutError: await ctx.send(f"Okay, I won't remove any of the {event} message formats.") return else: removed = messages.pop(num - 1) await ctx.send(f"Done. This {event} message format was deleted:\n`{removed}`") async def __message_list(self, ctx: commands.Context, event: str) -> None: """Handler for listing message formats.""" guild: discord.Guild = ctx.guild msg = f"{event.capitalize()} message formats:\n" messages = await self.config.guild(guild).get_attr(event).messages() for n, m in enumerate(messages, start=1): msg += f" {n}. {m}\n" for page in pagify(msg, shorten_by=20): await ctx.send(box(page)) async def __handle_event( self, guild: discord.guild, user: Union[discord.Member, discord.User], event: str, *, message_format=None ) -> None: """Handler for actual events.""" guild_settings = self.config.guild(guild) # always increment, even if we aren't sending a notice await self.__increment_count(guild, event) if await guild_settings.enabled(): settings = await guild_settings.get_attr(event).all() if settings["enabled"]: # notices for this event are enabled if settings["delete"] and settings["last"] is not None: # we need to delete the previous message await self.__delete_message(guild, settings["last"], event) # regardless of success, remove reference to that message await guild_settings.get_attr(event).last.set(None) # send a notice to the channel new_message = await self.__send_notice(guild, user, event, message_format=message_format) # store it for (possible) deletion later await guild_settings.get_attr(event).last.set(new_message and new_message.id) async def __get_channel(self, guild: discord.Guild, event: str) -> discord.TextChannel: """Gets the best text channel to use for event notices. Order of priority: 1. User-defined channel 2. Guild's system channel (if bot can speak in it) 3. First channel that the bot can speak in """ channel = None if event == "default": channel_id: int = await self.config.guild(guild).channel() else: channel_id = await self.config.guild(guild).get_attr(event).channel() if channel_id is not None: channel = guild.get_channel(channel_id) if channel is None or not Welcome.__can_speak_in(channel): channel = guild.get_channel(await self.config.guild(guild).channel()) if channel is None or not Welcome.__can_speak_in(channel): channel = guild.system_channel if channel is None or not Welcome.__can_speak_in(channel): for ch in guild.text_channels: if Welcome.__can_speak_in(ch): channel = ch break return channel async def __delete_message(self, guild: discord.Guild, message_id: int, event: str) -> None: """Attempts to delete the message with the given ID.""" try: await (await (await self.__get_channel(guild, event)).fetch_message(message_id)).delete() except discord.NotFound: log.warning("Failed to delete message (ID {message_id}): not found") except discord.Forbidden: log.warning("Failed to delete message (ID {message_id}): insufficient permissions") except discord.DiscordException: log.warning("Failed to delete message (ID {message_id})") async def __send_notice( self, guild: discord.guild, user: Union[discord.Member, discord.User], event: str, *, message_format=None ) -> Optional[discord.Message]: """Sends the notice for the event.""" format_str = message_format or await self.__get_random_message_format(guild, event) count = await self.config.guild(guild).get_attr(event).counter() plural = "" if count and count != 1: plural = "s" channel = await self.__get_channel(guild, event) if isinstance(user, discord.Member): roles = [r for r in user.roles if r.name != "@everyone"] roles.sort(reverse=True) roles = [r.name for r in roles] roles = humanize_list(roles) else: roles = [] actlog = self.bot.get_cog("ActivityLogger") if actlog: stats = await actlog.userstats(guild, user) stats = stats[0] else: stats = "" try: return await channel.send( format_str.format( member=user, server=guild, bot=user, count=count or "", plural=plural, roles=roles, stats=stats, joined_on=f"", ), allowed_mentions=discord.AllowedMentions.all(), ) except discord.Forbidden: log.error( f"Failed to send {event} message to channel ID {channel.id} (server ID {guild.id}): " "insufficient permissions" ) return None except discord.DiscordException: log.error(f"Failed to send {event} message to channel ID {channel.id} (server ID {guild.id})") return None except KeyError: log.error( f"Failed to send {event} message to channel ID {channel.id} (server id {guild.id}) because there is an error in message formatting." ) return await channel.send(f"{box(format_str)} has an unknown key in brackets. Please fix this format.") async def __get_random_message_format(self, guild: discord.guild, event: str) -> str: """Gets a random message for event of type event.""" async with self.config.guild(guild).get_attr(event).messages() as messages: return random.choice(messages) async def __increment_count(self, guild: discord.Guild, event: str) -> None: """Increments the counter for s today. Handles date changes.""" guild_settings = self.config.guild(guild) if await guild_settings.date() is None: await guild_settings.date.set(Welcome.__today()) if Welcome.__today() > await guild_settings.date(): await guild_settings.date.set(Welcome.__today()) await guild_settings.get_attr(event).counter.set(0) count: int = await guild_settings.get_attr(event).counter() await guild_settings.get_attr(event).counter.set(count + 1) async def __dm_user(self, member: discord.Member) -> None: """Sends a DM to the user with a filled-in message_format.""" message_format = await self.config.guild(member.guild).join.whisper.message() try: await member.send( message_format.format(member=member, server=member.guild), allowed_mentions=discord.AllowedMentions.all(), ) except discord.Forbidden: log.error( f"Failed to send DM to member ID {member.id} (server ID {member.guild.id}): insufficient permissions" ) raise WhisperError() except discord.DiscordException: log.error(f"Failed to send DM to member ID {member.id} (server ID {member.guild.id})") raise WhisperError() @staticmethod async def __get_number_input(ctx: commands.Context, maximum: int, minimum: int = 0) -> int: """Gets a number from the user, minimum < x <= maximum.""" author = ctx.author channel = ctx.channel def check(m: discord.Message) -> bool: try: num = int(m.content) except ValueError: return False return num is not None and minimum < num <= maximum and m.author == author and m.channel == channel try: msg = await ctx.bot.wait_for("message", check=check, timeout=15.0) except asyncio.TimeoutError: raise else: return int(msg.content) @staticmethod def __can_speak_in(channel: discord.TextChannel) -> bool: """Indicates whether the bot has permission to speak in channel.""" return channel.permissions_for(channel.guild.me).send_messages @staticmethod def __today() -> int: """Gets today's date in ordinal form.""" return datetime.date.today().toordinal() async def red_delete_data_for_user( self, *, requester: Literal["discord_deleted_user", "owner", "user", "user_strict"], user_id: int, ): pass