async def warn(self, ctx, member: discord.Member, *, reason): """ Warns a member. Reason length is maximum of 200 characters. """ if len(reason) > 200: await ctx.send( embed=failure("Please shorten the reason to 200 characters."), delete_after=3) return embed = infraction_embed(ctx, member, constants.Infraction.warning, reason) embed.add_field( name="**NOTE**", value=("If you are planning to repeat this again, " "the mods may administer punishment for the action.")) try: await self.bot.api_client.add_member_warning( ctx.author.id, member.id, reason) except Exception as e: msg = "Could not apply warning, problem with API." logger.info(f"{msg} {e}") await ctx.send(embed=failure( f"{msg}\nInfraction member should not think he got away.")) else: await self.deterrence_log_channel.send(f"{member.mention}", delete_after=0.5) await self.deterrence_log_channel.send(embed=embed) await ctx.send(embed=success("Warning successfully applied.", ctx.me), delete_after=5) await asyncio.sleep(5) await ctx.message.delete()
async def promote(self, ctx, member: discord.Member, role: discord.Role): """Promote member to role.""" if role >= ctx.author.top_role: await ctx.send( embed=failure("Role needs to be below you in hierarchy.")) return elif role in member.roles: await ctx.send(embed=failure( f"{member.mention} already has role {role.mention}!")) return await member.add_roles(role) await ctx.send(embed=success( f"{member.mention} is promoted to {role.mention}", ctx.me), delete_after=5) dm_embed = info(( f"You are now promoted to role **{role.name}** in our community.\n" f"`'With great power comes great responsibility'`\n" f"Be active and keep the community safe."), ctx.me, "Congratulations!") dm_embed.set_footer(text="Tortoise community") await member.send(embed=dm_embed)
async def _wait_for(self, container: set, user: discord.User) -> Union[discord.Message, None]: """ Simple custom wait_for that waits for user reply for 5 minutes and has ability to cancel the wait, deal with errors and deal with containers (which mark users that are currently doing something aka event submission/bug report etc). :param container: set, container holding active user sessions by having their IDs in it. :param user: Discord user to wait reply from :return: Union[Message, None] message representing user reply, can be none representing invalid reply. """ def check(msg): return msg.guild is None and msg.author == user container.add(user.id) await user.send(embed=info( "Reply with single message, link to paste service or uploading utf-8 `.txt` file.\n" "You have 5m, type `cancel` to cancel right away.", user)) try: user_reply = await self.bot.wait_for("message", check=check, timeout=300) except TimeoutError: container.remove(user.id) await user.send(embed=failure("You took too long to reply.")) return if user_reply.content.lower() == "cancel": container.remove(user.id) await user.send(embed=success("Successfully canceled.")) return return user_reply
async def dm_members(self, ctx, role: discord.Role, *, message: str): """ DMs all member that have a certain role. Failed members are printed to log. """ members = (member for member in role.members if not member.bot) failed = [] count = 0 for member in members: dm_embed = discord.Embed(title=f"Message for role {role}", description=message, color=role.color) dm_embed.set_author(name=ctx.guild.name, icon_url=ctx.guild.icon_url) try: await member.send(embed=dm_embed) except discord.HTTPException: failed.append(str(member)) else: count += 1 await ctx.send( embed=success(f"Successfully notified {count} users.", ctx.me)) if failed: logger.info(f"dm_unverified called but failed to dm: {failed}")
async def set_defcon_trigger(self, ctx, trigger: int): if not 7 <= trigger <= 100: return await ctx.send( embed=failure("Please use integer from 7 to 100.")) self.joins_per_min_trigger = trigger await ctx.send(embed=success( f"Successfully changed DEFCON trigger to {trigger} users/min."))
async def delete_suggestion(self, ctx, message_id: int): """Delete a suggestion""" msg: Message = await self.user_suggestions_channel.fetch_message(message_id) if msg is not None: await msg.delete() await self.bot.api_client.delete_suggestion(message_id) await ctx.send(embed=success("Suggestion successfully deleted."), delete_after=5)
async def ban(self, ctx, user: GetFetchUser, *, reason="Reason not stated."): """Bans member from the guild.""" await self._ban_helper(ctx, user, reason) await ctx.send(embed=success(f"{user} successfully banned."), delete_after=10)
async def unban(self, ctx, user: GetFetchUser, *, reason="Reason not stated."): """Unbans member from the guild.""" await ctx.guild.unban(user=user, reason=reason) await ctx.send(embed=success(f"{user} successfully unbanned."), delete_after=5)
async def create_bug_report(self, user: discord.User): user_reply = await self._get_user_reply(self.active_bug_reports, user) if user_reply is None: return await self.bug_report_channel.send( f"User `{user}` ID:{user.id} submitted bug report: {user_reply}") await user.send( embed=success("Bug report successfully submitted, thank you.")) self.active_bug_reports.remove(user.id)
async def clear(self, ctx, amount: int, member: discord.Member = None): """ Clears last X amount of messages. If member is passed it will clear last X messages from that member. """ def check(msg): return member is None or msg.author == member await ctx.channel.purge(limit=amount + 1, check=check) await ctx.send(embed=success(f"{amount} messages cleared."), delete_after=3)
async def create_suggestion(self, user: discord.User): user_reply = await self._get_user_reply(self.active_suggestions, user) if user_reply is None: return msg = await create_suggestion_msg(self.user_suggestions_channel, user, user_reply) await self.bot.api_client.post_suggestion(user, msg, user_reply) await user.send( embed=success("Suggestion successfully submitted, thank you.")) self.active_suggestions.remove(user.id)
async def mute(self, ctx, member: discord.Member, *, reason="No reason stated."): """Mutes the member.""" if self.muted_role in member.roles: await ctx.send(embed=failure("Cannot mute as member is already muted.")) return reason = f"Muting member. {reason}" await member.add_roles(self.muted_role, reason=reason) await member.remove_roles(self.verified_role, reason=reason) await ctx.send(embed=success(f"{member} successfully muted."), delete_after=5) await self.bot.api_client.add_member_warning(ctx.author.id, member.id, reason)
async def load(self, ctx, extension_name): """ Loads an extension. :param extension_name: cog name without suffix """ self.bot.load_extension(f"bot.cogs.{extension_name}") msg = f"{extension_name} loaded." logger.info(f"{msg} by {ctx.author.id}") await ctx.send(embed=success(msg, ctx.me))
async def reload(self, ctx, extension_name): """ Reloads an extension. :param extension_name: cog name without suffix """ if extension_name == Path(__file__).stem: await ctx.send(embed=failure( "This cog is protected, cannot execute operation.")) return self.bot.reload_extension(f"bot.cogs.{extension_name}") await ctx.send(embed=success(f"{extension_name} reloaded.", ctx.me))
async def create_event_submission(self, user: discord.User): user_reply = await self._get_user_reply(self.active_event_submissions, user) if user_reply is None: return await self.code_submissions_channel.send( f"User `{user}` ID:{user.id} submitted code submission: " f"{user_reply}") await user.send( embed=success("Event submission successfully submitted.")) self.active_event_submissions.remove(user.id)
async def unmute(self, ctx, member: discord.Member): """Unmutes the member.""" if self.muted_role not in member.roles: await ctx.send(embed=failure("Cannot unmute as member is not muted.")) return reason = f"Unmuted by {ctx.author.id}" await member.remove_roles(self.muted_role, reason=reason) await member.add_roles(self.verified_role, reason=reason) await ctx.send(embed=success(f"{member} successfully unmuted."), delete_after=5)
async def _mass_ban_timestamp_helper(self, ctx, timestamp_start: datetime, timestamp_end: datetime, reason: str): members_to_ban = [] for member in self.tortoise_guild.members: if member.joined_at is None: continue if timestamp_start < member.joined_at < timestamp_end: members_to_ban.append(member) if not members_to_ban: return await ctx.send(embed=failure("Could not find any members, aborting..")) members_to_ban.sort(key=lambda m: m.joined_at) reaction_msg = await ctx.send( embed=warning( f"This will ban {len(members_to_ban)} members, " f"first one being {members_to_ban[0]} and last one being {members_to_ban[-1]}.\n" f"Are you sure you want to continue?" ) ) confirmation = await ConfirmationMessage.create_instance(self.bot, reaction_msg, ctx.author) if confirmation: one_tenth = len(members_to_ban) // 10 notify_interval = one_tenth if one_tenth > 50 else 50 await ctx.send( embed=info( f"Starting the ban process, please be patient.\n" f"You will be notified for each {notify_interval} banned members.", ctx.author ) ) logger.info(f"{ctx.author} is timestamp banning: {', '.join(str(member.id) for member in members_to_ban)}") for count, member in enumerate(members_to_ban): if count != 0 and count % notify_interval == 0: await ctx.send(embed=info(f"Banned {count} members..", ctx.author)) await ctx.guild.ban(member, reason=reason) message = f"Successfully mass banned {len(members_to_ban)} members!" await ctx.send(embed=success(message)) await self.deterrence_log_channel.send(embed=authored(message, author=ctx.author)) else: await ctx.send(embed=info("Aborting mass ban.", ctx.me))
async def kick(self, ctx, member: discord.Member, *, reason="No specific reason"): """Kicks member from the guild.""" await member.kick(reason=reason) await ctx.send(embed=success(f"{member.name} successfully kicked."), delete_after=5) deterrence_embed = infraction_embed(ctx, member, constants.Infraction.kick, reason) await self.deterrence_log_channel.send(embed=deterrence_embed) dm_embed = deterrence_embed dm_embed.add_field( name="Repeal", value="If this happened by a mistake contact moderators." ) await member.send(embed=dm_embed)
async def ban_timestamp(self, ctx, timestamp_start: DatetimeConverter, timestamp_end: DatetimeConverter, *, reason="Mass ban with timestamp."): """Bans member from the guild if he joined at specific time. Both arguments need to be in this specific format: %Y-%m-%d %H:%M Example: t.ban_timestamp "2020-09-15 13:00" "2020-10-15 13:00" All values need to be padded with 0. Timezones are not accounted for. """ members_to_ban = [] for member in self.tortoise_guild.members: if member.joined_at is None: continue if timestamp_start < member.joined_at < timestamp_end: members_to_ban.append(member) if not members_to_ban: return await ctx.send( embed=failure("Could not find any members, aborting..")) reaction_msg = await ctx.send(embed=warning( f"This will ban {len(members_to_ban)} members, " f"first one being {members_to_ban[0]} and last one being {members_to_ban[-1]}.\n" f"Are you sure you want to continue?")) confirmation = await ConfirmationMessage.create_instance( self.bot, reaction_msg, ctx.author) if confirmation: logger.info( f"{ctx.author} is timestamp banning: {', '.join(member.id for member in members_to_ban)}" ) for member in members_to_ban: await self._ban_helper(ctx, member, reason) await ctx.send(embed=success( f"Successfully mass banned {len(members_to_ban)} members!")) else: await ctx.send(embed=info("Aborting mass ban.", ctx.me))
async def create_mod_mail(self, user: discord.User): if user.id in self.pending_mod_mails: await user.send(embed=failure( "You already have a pending mod mail, please be patient.")) return submission_embed = authored(f"`{user.id}` submitted for mod mail.", author=user) # Ping roles so they get notified sooner await self.mod_mail_report_channel.send("@here", delete_after=30) await self.mod_mail_report_channel.send(embed=submission_embed) self.pending_mod_mails.add(user.id) await user.send(embed=success( "Mod mail was sent to admins, please wait for one of the admins to accept." ))
async def unload(self, ctx, extension_name): """ Unloads an extension. :param extension_name: cog name without suffix """ if extension_name == Path(__file__).stem: await ctx.send( embed=failure("This cog is protected, cannot unload.")) return self.bot.unload_extension(f"bot.cogs.{extension_name}") msg = f"{extension_name} unloaded." logger.info(f"{msg} by {ctx.author.id}") await ctx.send(embed=success(f"{extension_name} unloaded.", ctx.me))
async def on_raw_reaction_add(self, payload): if payload.channel_id == constants.react_for_roles_channel_id: guild = self.bot.get_guild(payload.guild_id) member = guild.get_member(payload.user_id) role = self.get_assignable_role(payload, guild) if member.id == self.bot.user.id: return # Ignore the bot elif role is not None: await member.add_roles(role) embed = success(f"`{role.name}` has been assigned to you in the Tortoise community.") await member.send(embed=embed, delete_after=10) elif payload.channel_id == constants.suggestions_channel_id: if payload.emoji.id == constants.suggestions_emoji_id: await self.bot.get_cog("TortoiseDM").on_raw_reaction_add_helper(payload)
async def verify_member(self, member_id: str): """ Adds verified role to the member and also sends success messages. :param member_id: str member id to verify """ try: member_id = int(member_id) except ValueError: raise EndpointBadArguments() none_checks = (self.tortoise_guild, self.verified_role, self.new_member_role, self.successful_verifications_channel, self.welcome_channel) for check_none in none_checks: if check_none is None: logger.warning( f"One of necessary IDs was not found {none_checks}") raise DiscordIDNotFound() # Attempt to fix bug with verification where sometimes member is not found in cache even if they are in guild tortoise_guild = self.bot.get_guild(constants.tortoise_guild_id) member = tortoise_guild.get_member(member_id) if member is None: logger.critical( f"Can't verify, member is not found in guild {member} {member_id}" ) raise DiscordIDNotFound() await member.add_roles(self.verified_role, self.new_member_role, reason="Completed Oauth2 Verification") await self.successful_verifications_channel.send( embed=info(f"{member} is now verified.", member.guild.me, title="") ) msg = (f"You are now verified {self.verified_emoji}\n\n" f"Make sure to read {self.welcome_channel.mention}") await self.general_channel.send( member.mention, embed=info(f"Say hi to our newest member {member.mention}", member.guild.me, title=""), delete_after=100) await member.send(embed=success(msg))
async def ban(self, ctx, user: GetFetchUser, *, reason="Reason not stated."): """Bans member from the guild.""" await ctx.guild.ban(user=user, reason=reason) await ctx.send(embed=success(f"{user} successfully banned."), delete_after=5) deterrence_embed = infraction_embed(ctx, user, constants.Infraction.ban, reason) await self.deterrence_log_channel.send(embed=deterrence_embed) dm_embed = deterrence_embed dm_embed.add_field( name="Repeal", value="If this happened by a mistake contact moderators.") await user.send(embed=dm_embed)
async def connect_(self, ctx, *, channel: discord.VoiceChannel = None): """Connect to voice. Parameters ------------ channel: discord.VoiceChannel [Optional] The channel to connect to. If a channel is not specified, an attempt to join the voice channel you are in will be made. This command also handles moving the bot to different channels. Note - The channel has to have 'music' in it's name. This is to avoid spamming music in general voice chats. """ if not channel: try: channel = ctx.author.voice.channel except AttributeError: raise InvalidVoiceChannel( "No channel to join. Please either specify a valid channel or join one." ) if "music" not in channel.name.lower(): raise InvalidVoiceChannel( "Can't join channel - channel has to have 'music' in it's name." ) vc = ctx.voice_client if vc: if vc.channel.id == channel.id: return try: await vc.move_to(channel) except asyncio.TimeoutError: raise VoiceConnectionError( f"Moving to channel: <{channel}> timed out.") else: try: await channel.connect() except asyncio.TimeoutError: raise VoiceConnectionError( f"Connecting to channel: <{channel}> timed out.") await ctx.send(embed=success(f"Connected to: **{channel}**", ctx.me))
async def deny(self, ctx, message_id: int, *, reason: str = "No reason specified"): """Deny a suggestion""" await self._suggestion_helper(ctx, message_id, reason, constants.SuggestionStatus.denied) await ctx.send(embed=success("Suggestion successfully denied."), delete_after=5)
async def disable_defcon(self, ctx): self.defcon_active = False await ctx.send(embed=success( f"Successfully deactivated DEFCON.\n" f"Kicked user count: {self._kicked_while_defcon_was_active}")) self._kicked_while_defcon_was_active = 0
async def attend(self, ctx, user_id: int): if not any(role in ctx.author.roles for role in (self.admin_role, self.moderator_role)): await ctx.send(embed=failure( "You do not have permission to use this command.")) return # Time to wait for FIRST USER reply. Useful if mod attends but user is away. first_timeout = 21_600 # 6 hours # Flag for above variable. False means there has been no messages from the user. first_timeout_flag = False # After the user sends first reply this is the timeout we use. regular_timeout = 1800 # 30 min user = self.bot.get_user(user_id) mod = ctx.author if user is None: await ctx.send(embed=failure( "That user cannot be found or you entered incorrect ID.")) return elif user_id not in self.pending_mod_mails: await ctx.send( embed=failure("That user is not registered for mod mail.")) return elif self.is_any_session_active(mod.id): await ctx.send(embed=failure( "You already have one of active sessions (reports/mod mail etc)." )) return try: await mod.send( embed=success(f"You have accepted `{user}` mod mail request.\n" "Reply here in DMs to chat with them.\n" "This mod mail will be logged.\n" "Type `close` to close this mod mail.")) except discord.HTTPException: await ctx.send(embed=failure( "Mod mail failed to initialize due to mod having closed DMs.")) return # Unlike failing for mods due to closed DMs this cannot fail for user since user already did interact # with bot in DMs as he needs to in order to even open mod-mail. await user.send(embed=authored(( "has accepted your mod mail request.\n" "Reply here in DMs to chat with them.\n" "This mod mail will be logged, by continuing you agree to that."), author=mod)) await ctx.send(embed=success("Mod mail initialized, check your DMs.")) self.pending_mod_mails.remove(user_id) self.active_mod_mails[user_id] = mod.id _timeout = first_timeout # Keep a log of all messages in mod-mail log = MessageLogger(mod.id, user.id) def mod_mail_check(msg): return msg.guild is None and msg.author.id in (user_id, mod.id) while True: try: mail_msg = await self.bot.wait_for("message", check=mod_mail_check, timeout=_timeout) log.add_message(mail_msg) except TimeoutError: timeout_embed = failure("Mod mail closed due to inactivity.") log.add_embed(timeout_embed) await mod.send(embed=timeout_embed) await user.send(embed=timeout_embed) del self.active_mod_mails[user_id] await self.mod_mail_report_channel.send(file=discord.File( StringIO(str(log)), filename=log.filename)) break # Deal with attachments. We don't re-upload we just copy paste attachment url. attachments = self._get_attachments_as_urls(mail_msg) mail_msg.content += attachments if len(mail_msg.content) > 1900: mail_msg.content = f"{mail_msg.content[:1900]} ...truncated because it was too long." # Deal with dynamic timeout. if mail_msg.author == user and not first_timeout_flag: first_timeout_flag = True _timeout = regular_timeout # Deal with canceling mod mail if mail_msg.content.lower( ) == "close" and mail_msg.author.id == mod.id: close_embed = success( f"Mod mail successfully closed by {mail_msg.author}.") log.add_embed(close_embed) await mod.send(embed=close_embed) await user.send(embed=close_embed) del self.active_mod_mails[user_id] await self.mod_mail_report_channel.send(file=discord.File( StringIO(str(log)), filename=log.filename)) break # Deal with user-mod communication if mail_msg.author == user: await mod.send(mail_msg.content) elif mail_msg.author == mod: await user.send(mail_msg.content)