async def rule(self, ctx, alias: Union[int, str]): """Shows rule based on number order or alias.""" if isinstance(alias, int): rule_dict = self._get_rule_by_value(alias) else: rule_dict = self._get_rule_by_alias(alias) if rule_dict is None: await ctx.send(embed=failure("No such rule."), delete_after=5) else: await ctx.send(embed=info(rule_dict["statement"], ctx.guild.me, f"Rule: {rule_dict['name']}"))
async def cog_command_error(self, ctx, error): """A local error handler for all errors arising from commands in this cog.""" if isinstance(error, commands.NoPrivateMessage): try: return await ctx.send(embed=failure( "This command can not be used in private messages.")) except discord.HTTPException: pass elif isinstance(error, InvalidVoiceChannel): await ctx.send(embed=failure( "Error connecting to Voice Channel. " "Please make sure you are in a valid channel or provide me with one" )) elif isinstance(error, TortoiseGuildCheckFailure): await ctx.send(embed=failure(f"{error}")) else: traceback_msg = traceback.format_exception(etype=type(error), value=error, tb=error.__traceback__) logger.error(traceback_msg) await self.bot.log_error(traceback_msg)
async def queue_info(self, ctx): """Retrieve a basic queue of upcoming songs.""" vc = ctx.voice_client if not vc or not vc.is_connected(): return await ctx.send( embed=failure("I am not currently connected to voice!")) player = self.get_player(ctx) if player.queue.empty(): return await ctx.send( embed=failure("There are currently no more queued songs.")) # Grab up to 5 entries from the queue... upcoming = list(itertools.islice(player.queue._queue, 0, 5)) fmt = "\n".join(f"**`{_['title']}`**" for _ in upcoming) embed = discord.Embed(title=f"Upcoming - Next {len(upcoming)}", description=fmt) await ctx.send(embed=embed)
async def stop_(self, ctx): """Stop the currently playing song and destroy the player. !Warning! This will destroy the player assigned to your guild, also deleting any queued songs and settings. """ vc = ctx.voice_client if not vc or not vc.is_connected(): return await ctx.send( embed=failure("I am not currently playing anything!")) await self.cleanup(ctx.guild)
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 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 pause_(self, ctx): """Pause the currently playing song.""" vc = ctx.voice_client if not vc or not vc.is_playing(): return await ctx.send( embed=failure("I am not currently playing anything!")) elif vc.is_paused(): return vc.pause() await ctx.send(embed=info(f"**`{ctx.author}`** paused the song.", ctx.me, title="Song paused"))
async def resume_(self, ctx): """Resume the currently paused song.""" vc = ctx.voice_client if not vc or not vc.is_connected(): return await ctx.send( embed=failure("I am not currently playing anything!")) elif not vc.is_paused(): return vc.resume() await ctx.send(embed=info(f"**`{ctx.author}`** resumed the song.", ctx.me, title="Song resumed"))
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 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 skip_(self, ctx): """Skip the song.""" vc = ctx.voice_client if not vc or not vc.is_connected(): return await ctx.send( embed=failure("I am not currently playing anything!")) if vc.is_paused(): pass elif not vc.is_playing(): return vc.stop() await ctx.send(embed=info(f"**`{ctx.author}`** skipped the song.", ctx.me, title="Skipping song"))
async def aoc_countdown(self, ctx): """Time until next challenge starts.""" utc_minus_5 = datetime.timezone(offset=datetime.timedelta(hours=-5)) now = datetime.datetime.now(tz=utc_minus_5) if now.month != 12: return await ctx.send(embed=failure("AoC is over!")) current_day = now.day end_date = datetime.datetime(year=2020, month=12, day=current_day + 1, tzinfo=utc_minus_5) difference = end_date - now ends_in = format_timedelta(difference) await ctx.send(embed=info(f"Day {current_day} ends in {ends_in}", title="Countdown", member=ctx.guild.me))
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 player_loop(self): """Our main player loop.""" await self.bot.wait_until_ready() while not self.bot.is_closed(): self.next.clear() try: # Wait for the next song. If we timeout cancel the player and disconnect... async with timeout(300): source = await self.queue.get() except asyncio.TimeoutError: return self.destroy(self._guild) if not isinstance(source, YTDLSource): # Source was probably a stream (not downloaded) # So we should regather to prevent stream expiration try: source = await YTDLSource.regather_stream( source, loop=self.bot.loop) except Exception as e: await self._channel.send(embed=failure( f"There was an error processing your song.\n" f"```css\n[{e}]```")) continue source.volume = self.volume self.current = source self._guild.voice_client.play(source, after=lambda _: self.bot.loop. call_soon_threadsafe(self.next.set)) self.now_playing = f"`{source.title}` requested by `{source.requester}`" await self._channel.send(embed=info( self.now_playing, self._guild.me, title="Now playing")) await self.next.wait() # Make sure the FFmpeg process is cleaned up. source.cleanup() self.current = None self.now_playing = "Nothing."
async def hata(self, ctx, *, search_for: str): """Shows hata docs based on search query.""" hata_docs_website = "https://huyanematsu.pythonanywhere.com/docs/" results = await self.hata_api.search(search_for) if not results: return await ctx.send( embed=failure(f"Could not find anything for {search_for}.")) body = [] for result in results[:8]: name_with_link = f"[{result['name']}]({hata_docs_website}{result['url']})" body.append( f"{name_with_link} *{result['type']}*\n{result.get('preview', '')}" ) description = "\n\n".join(body) title = f"Showing {len(results)}" if len( results) < 8 else f"Showing 8/{len(results)}" docs_embed = discord.Embed(title=title, description=description) await ctx.send(embed=docs_embed)
async def fetch_doc_links(self, ctx, key, obj): page_types = { 'latest': 'https://discordpy.readthedocs.io/en/latest', 'python': 'https://docs.python.org/3', } if obj is None: await ctx.send(page_types[key]) return if not self._doc_cache: await ctx.trigger_typing() await self.build_documentation_lookup_table(page_types) obj = re.sub(r'^(?:discord\.(?:ext\.)?)?(?:commands\.)?(.+)', r'\1', obj) if key.startswith('latest'): # point the abc.Messageable types properly: q = obj.lower() for name in dir(discord.abc.Messageable): if name[0] == '_': continue if q == name: obj = f'abc.Messageable.{name}' break cache = list(self._doc_cache[key].items()) matches = Fuzzy.finder(obj, cache, key=lambda t: t[0], lazy=False)[:8] if len(matches) == 0: await ctx.send(embed=failure("Query didn't match any entity")) return embed_msg = "\n".join(f"[`{key}`]({url})" for key, url in matches) embed_msg = info(embed_msg, ctx.me, title="Links") await ctx.send(embed=embed_msg)
async def leaderboard(self, ctx): """ Shows Tortoise leaderboard. Leaderboard is updated each 30 minutes. """ if self._leaderboard_cache is None: return await ctx.send(embed=failure( "Please try again in few seconds as cache is not yet loaded.")) sorted_members = { k: v for k, v in sorted(self._leaderboard_cache["members"].items(), key=lambda item: item[1]["local_score"], reverse=True) } leaderboard = ["```py"] num_of_members = 10 position_counter = 0 for member_id, member_data in sorted_members.items(): position_counter += 1 if position_counter > num_of_members: break stars_pretty = f"{'★' + str(member_data['stars']):4}" leaderboard.append( f"{position_counter}. {member_data['local_score']:4}p {stars_pretty} {member_data['name']}" ) leaderboard.append("```") leaderboard_text = "\n".join(leaderboard) embed = info( f"{leaderboard_text}\n\nThe leaderboard is refreshed each 30 minutes.", member=ctx.guild.me, title="Tortoise AoC leaderboard") await ctx.send(embed=embed)
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)