async def leave(self, ctx, *arg): """ `!leave` __`Removes an existing role from yourself`__ **Usage:** !leave [role name] **Examples:** `!leave L2A` removes the L2A role from yourself """ # case where role name is space separated name = " ".join(arg).lower() if not name: raise BadArgs("", show_help=True) aliases = {"he": "he/him/his", "she": "she/her/hers", "ze": "ze/zir/zirs", "they": "they/them/theirs"} # Convert alias to proper name if name.lower() in aliases: name = aliases[name] role = next((r for r in ctx.guild.roles if name == r.name.lower()), None) if not role: raise BadArgs("that role doesn't exist!") if role not in ctx.author.roles: raise BadArgs("you don't have that role!") await ctx.author.remove_roles(role) await ctx.send("role removed!", delete_after=5)
async def leave(self, ctx: commands.Context, *arg: str): """ `!leave` __`Removes an existing role from yourself`__ **Usage:** !leave [role name] **Examples:** `!leave Study Group` removes the Study Group role from yourself """ # case where role name is space separated name = " ".join(arg).lower() if not name: raise BadArgs("", show_help=True) try: role = await self.role_converter.convert(ctx, name) except commands.RoleNotFound: raise BadArgs("That role doesn't exist!", show_help=True) if role not in ctx.author.roles: raise BadArgs("you don't have that role!") await ctx.author.remove_roles(role) await ctx.send("role removed!", delete_after=5)
async def ensure_voice_state(self, ctx: commands.Context): if not ctx.author.voice or not ctx.author.voice.channel: raise BadArgs("You are not connected to any voice channel.") if ctx.voice_client: if ctx.voice_client.channel != ctx.author.voice.channel: raise BadArgs("Bot is already in a voice channel.")
async def dm(self, ctx): """ `!dm` __`213DM Generator`__ **Usage:** !dm <user | close> [user] [...] **Examples:** `!dm @blankuser#1234` creates 213DM with TAs and blankuser `!dm @blankuser#1234 @otheruser#5678` creates 213DM with TAs, blankuser and otheruser `!dm close` closes 213DM """ # meant for 213 server guild = self.bot.get_guild(796222302483251241) if "close" in ctx.message.content.lower(): if not ctx.channel.name.startswith("213dm-"): raise BadArgs("This is not a 213DM.") await ctx.send("Closing 213DM.") await next(i for i in guild.roles if i.name == ctx.channel.name).delete() return await ctx.channel.delete() if not ctx.message.mentions: raise BadArgs("You need to specify a user or users to add!", show_help=True) # check that nobody is already in a 213dm before going and creating everything for user in ctx.message.mentions: for role in user.roles: if role.name.startswith("213dm"): raise BadArgs(f"{user.name} is already in a 213DM.") # generate customized channel name to allow customized role nam = int(str((datetime.now() - datetime(1970, 1, 1)).total_seconds()).replace(".", "")) + ctx.author.id nam = f"213dm-{nam}" # create custom role role = await guild.create_role(name=nam, colour=discord.Colour(0x88ff88)) for user in ctx.message.mentions: try: await user.add_roles(role) except (discord.Forbidden, discord.HTTPException): pass # if for whatever reason one of the people doesn't exist, just ignore and keep going access = discord.PermissionOverwrite(read_messages=True, send_messages=True, read_message_history=True) noaccess = discord.PermissionOverwrite(read_messages=False, read_message_history=False, send_messages=False) overwrites = { # allow Computers and the new role, deny everyone else guild.default_role : noaccess, guild.get_role(796222302697816121): access, role : access } # this id is id of group dm category channel = await guild.create_text_channel(nam, overwrites=overwrites, category=guild.get_channel(796505656437768233)) await ctx.send("Opened channel.") users = (f"<@{usr.id}>" for usr in ctx.message.mentions) await channel.send(f"<@{ctx.author.id}> {' '.join(users)}\n" + f"Welcome to 213 private DM. Type `!dm close` to exit when you are finished.")
async def userstats(self, ctx, *userid): """ `!userstats` __`Check user profile and stats`__ **Usage:** !userstats <USER ID> **Examples:** `!userstats 375445489627299851` [embed] """ if not userid: user = ctx.author else: try: userid = int(userid[0]) except ValueError: raise BadArgs("Please enter a user id", show_help=True) user = ctx.guild.get_member(userid) if not user: raise BadArgs("That user does not exist") # we use both user and member objects, since some stats can only be obtained # from either user or member object async with ctx.channel.typing(): most_active_channel = 0 most_active_channel_name = None cum_message_count = 0 yesterday = (datetime.now() - timedelta(days=1)).replace(tzinfo=pytz.timezone("US/Pacific")).astimezone(timezone.utc).replace(tzinfo=None) for channel in ctx.guild.text_channels: counter = 0 async for message in channel.history(after=yesterday, limit=None): if message.author == user: counter += 1 cum_message_count += 1 if counter > most_active_channel: most_active_channel = counter most_active_channel_name = "#" + channel.name embed = discord.Embed(title=f"Report for user `{user.name}#{user.discriminator}` (all times in UTC)") embed.add_field(name="Date Joined", value=user.joined_at.strftime("%A, %Y %B %d @ %H:%M:%S"), inline=True) embed.add_field(name="Account Created", value=user.created_at.strftime("%A, %Y %B %d @ %H:%M:%S"), inline=True) embed.add_field(name="Roles", value=", ".join([str(i) for i in sorted(user.roles[1:], key=lambda role: role.position, reverse=True)]), inline=True) embed.add_field(name="Most active text channel in last 24 h", value=f"{most_active_channel_name} ({most_active_channel} messages)", inline=True) embed.add_field(name="Total messages sent in last 24 h", value=str(cum_message_count), inline=True) await ctx.send(embed=embed)
async def join(self, ctx, *arg): """ `!join` __`Adds a role to yourself`__ **Usage:** !join [role name] **Examples:** `!join L2A` adds the L2A role to yourself **Valid Roles:** L2A, L2B, L2C, L2D, L2E, L2F, L2G, L2H, L2J, L2K, L2L, L2M, L2N, L2P, L2Q, L2R, L2S, He/Him/His, She/Her/Hers, They/Them/Theirs, Ze/Zir/Zirs """ # case where role name is space separated name = " ".join(arg).lower() # Display help if given no argument if not name: raise BadArgs("", show_help=True) # make sure that you can't add roles like "prof" or "ta" valid_roles = ["L2A", "L2B", "L2C", "L2D", "L2E", "L2F", "L2G", "L2H", "L2J", "L2K", "L2L", "L2M", "L2N", "L2P", "L2Q", "L2R", "L2S", "He/Him/His", "She/Her/Hers", "They/Them/Theirs", "Ze/Zir/Zirs"] aliases = {"he": "He/Him/His", "she": "She/Her/Hers", "ze": "Ze/Zir/Zirs", "they": "They/Them/Theirs"} # Convert alias to proper name if name.lower() in aliases: name = aliases[name].lower() # Ensure that people only add one lab role if name.startswith("l2") and any(role.name.startswith("L2") for role in ctx.author.roles): raise BadArgs("You already have a lab role!") # Grab the role that the user selected role = next((r for r in ctx.guild.roles if name == r.name.lower()), None) # Check that the role actually exists if not role: raise BadArgs("You can't add that role!", show_help=True) # Ensure that the author does not already have the role if role in ctx.author.roles: raise BadArgs("you already have that role!") # Special handling for roles that exist but can not be selected by a student if role.name not in valid_roles: self.add_instructor_role_counter += 1 if self.add_instructor_role_counter > 5: if self.add_instructor_role_counter == 42: if random.random() > 0.999: raise BadArgs("Congratulations, you found the secret message. IDEK how you did it, but good job. Still can't add the instructor role though. Bummer, I know.") elif self.add_instructor_role_counter == 69: if random.random() > 0.9999: raise BadArgs("nice.") raise BadArgs("You can't add that role, but if you try again, maybe something different will happen on the 42nd attempt") else: raise BadArgs("you cannot add an instructor/invalid role!", show_help=True) await ctx.author.add_roles(role) await ctx.send("role added!", delete_after=5)
async def skip(self, ctx: commands.Context): """ `!skip` __`Skip song`__ **Aliases:** s **Usage:** !skip **Examples:** `!skip` skips current song **Note:** requires at least 3 votes. """ if not self.voice_state.is_playing: raise BadArgs("Not playing any music right now...") voter = ctx.message.author if voter == self.voice_state.current.requester: await ctx.message.add_reaction("⏭") self.voice_state.skip() elif voter.id not in self.voice_state.skip_votes: self.voice_state.skip_votes.add(voter.id) total_votes = len(self.voice_state.skip_votes) if total_votes >= 3 or voter.id in self.bot.owner_ids: await ctx.message.add_reaction("⏭") self.voice_state.skip() else: await ctx.send( f"Skip vote added, currently at **{total_votes}/3**") else: await ctx.send("You have already voted to skip this song.")
async def queue(self, ctx: commands.Context, *, page: int = 1): """ `!queue` __`Song queue`__ **Aliases:** q **Usage:** !queue **Examples:** `!queue` shows current queue """ if not self.voice_state.songs: raise BadArgs("Empty queue.") items_per_page = 10 pages = math.ceil(len(self.voice_state.songs) / items_per_page) start = (page - 1) * items_per_page end = start + items_per_page queue = "" for i, song in enumerate(self.voice_state.songs[start:end], start=start): queue += f"`{i + 1}.` [**{song.source.title}**]({song.source.url})\n" embed = discord.Embed( description=f"**{len(self.voice_state.songs)} tracks:**\n\n{queue}", colour=random.randint(0, 0xFFFFFF)) embed.set_footer(text=f"Viewing page {page}/{pages}") await ctx.send(embed=embed)
async def unlive(self, ctx: commands.Context): """ `!unlive` Disables course tracking for the channel the command is invoked in. """ c_handler = self._get_canvas_handler(ctx.message.guild) if not isinstance(c_handler, CanvasHandler): raise BadArgs("Canvas Handler doesn't exist.") if ctx.message.channel in c_handler.live_channels: c_handler.live_channels.remove(ctx.message.channel) self.canvas_dict[str(ctx.message.guild.id)]["live_channels"] = [ channel.id for channel in c_handler.live_channels ] writeJSON(self.canvas_dict, "data/canvas.json") for course in c_handler.courses: watchers_file = f"{util.canvas_handler.COURSES_DIRECTORY}/{course.id}/watchers.txt" CanvasHandler.delete_channels_from_file([ctx.message.channel], watchers_file) # If there are no more channels watching the course, we should delete that course's directory. if os.stat(watchers_file).st_size == 0: shutil.rmtree( f"{util.canvas_handler.COURSES_DIRECTORY}/{course.id}") await ctx.send("Removed channel from live tracking.") else: await ctx.send("Channel was not live tracking.")
async def ppinned(self, ctx): """ `!ppinned` **Usage:** !ppinned **Examples:** `!ppinned` sends a list of the Piazza's pinned posts to the calling channel *to prevent hitting the rate-limit, only usable once every 5 secs channel-wide* """ if self.bot.d_handler.piazza_handler: posts = self.bot.d_handler.piazza_handler.get_pinned() embed = discord.Embed( title= f"**Pinned posts for {self.bot.d_handler.piazza_handler.course_name}:**", colour=0x497aaa) for post in posts: embed.add_field(name=f"@{post['num']}", value=f"[{post['subject']}]({post['url']})", inline=False) embed.set_footer(text=f"Requested by {ctx.author.display_name}", icon_url=str(ctx.author.avatar_url)) await ctx.send(embed=embed) else: raise BadArgs("Piazza hasn't been instantiated yet!")
async def help(self, ctx, *arg): """ `!help` __`Returns list of commands or usage of command`__ **Usage:** !help [optional cmd] **Examples:** `!help` [embed] """ if not arg: embed = discord.Embed(title="CS213 Bot", description="Commands:", colour=random.randint(0, 0xFFFFFF), timestamp=datetime.utcnow()) embed.add_field(name=f"❗ Current Prefix: `{self.bot.command_prefix}`", value="\u200b", inline=False) for k, v in self.bot.cogs.items(): embed.add_field(name=k, value=" ".join(f"`{i}`" for i in v.get_commands() if not i.hidden), inline=False) embed.set_thumbnail(url=self.bot.user.avatar_url) embed.add_field(name = "_ _\nSupport Bot Development: visit the CS213Bot repo at https://github.com/jbrightuniverse/cs213bot/", value = "_ _\nCS213Bot is based on CS221Bot. Support them at https://github.com/Person314159/cs221bot/\n\nCall ++help to access C++Bot from within this bot.\nhttps://github.com/jbrightuniverse/C-Bot") embed.set_footer(text=f"The sm213 language was created by Dr. Mike Feeley of the CPSC department at UBCV.\nUsed with permission.\n\nRequested by {ctx.author.display_name}", icon_url=str(ctx.author.avatar_url)) await ctx.send(embed=embed) else: help_command = arg[0] comm = self.bot.get_command(help_command) if not comm or not comm.help or comm.hidden: raise BadArgs("That command doesn't exist.") await ctx.send(comm.help)
async def join(self, ctx, *arg): """ `!join` __`Adds a role to yourself`__ **Usage:** !join [role name] **Examples:** `!join notify` adds the notify role to yourself **Valid Roles:** notify (`!join notify`), He/Him/His (`!join he`), She/Her/Hers (`!join she`), They/Them/Theirs (`!join they`), Ze/Zir/Zirs (`!join ze`) """ # case where role name is space separated name = " ".join(arg).lower() # Display help if given no argument if not name: raise BadArgs("", show_help=True) # make sure that you can't add roles like "prof" or "ta" valid_roles = ["L1A", "L1B", "L1C", "L1D", "L1E", "L1F", "L1G", "notify", "He/Him/His", "She/Her/Hers", "They/Them/Theirs", "Ze/Zir/Zirs"] aliases = {"he": "He/Him/His", "she": "She/Her/Hers", "ze": "Ze/Zir/Zirs", "they": "They/Them/Theirs"} # Convert alias to proper name if name.lower() in aliases: name = aliases[name].lower() # Grab the role that the user selected role = next((r for r in ctx.guild.roles if name == r.name.lower()), None) # Check that the role actually exists if not role: raise BadArgs("You can't add that role!", show_help=True) # Ensure that the author does not already have the role if role in ctx.author.roles: raise BadArgs("you already have that role!") # Special handling for roles that exist but can not be selected by a student if role.name not in valid_roles: raise BadArgs("you cannot add an instructor/invalid role!", show_help=True) await ctx.author.add_roles(role) await ctx.send("role added!", delete_after=5)
async def asgn(self, ctx: commands.Context, *args): """ `!asgn ( | (-due (n-(hour|day|week|month|year)) | YYYY-MM-DD | YYYY-MM-DD-HH:MM:SS) | -all)` Argument can be left blank for sending assignments due 2 weeks from now. *Filter till due date:* `!asgn -due` can be in time from now e.g.: `-due 4-hour` or all assignments before a certain date e.g.: `-due 2020-10-21` *All assignments:* `!asgn -all` returns ALL assignments. """ c_handler = self._get_canvas_handler(ctx.message.guild) if not isinstance(c_handler, CanvasHandler): raise BadArgs("Canvas Handler doesn't exist.") if args and args[0].startswith("-due"): due = args[1] course_ids = args[2:] elif args and args[0].startswith("-all"): due = None course_ids = args[1:] else: due = "2-week" course_ids = args assignments = c_handler.get_assignments(due, course_ids, CANVAS_API_URL) if not assignments: pattern = r"\d{4}-\d{2}-\d{2}" return await ctx.send( f"No assignments due by {due}{' (at 00:00)' if re.match(pattern, due) else ''}." ) for data in assignments: embed_var = discord.Embed(title=data[2], url=data[3], description=data[4], color=CANVAS_COLOR, timestamp=datetime.strptime( data[5], "%Y-%m-%d %H:%M:%S")) embed_var.set_author(name=data[0], url=data[1]) embed_var.set_thumbnail(url=CANVAS_THUMBNAIL_URL) embed_var.add_field(name="Due at", value=data[6], inline=True) embed_var.set_footer(text="Created at", icon_url=CANVAS_THUMBNAIL_URL) await ctx.send(embed=embed_var)
async def pread(self, ctx, postID): """ `!pread` __`post id`__ **Usage:** !pread <post id> **Examples:** `!pread 828` returns an embed with the [post](https://piazza.com/class/ke1ukp9g4xx6oi?cid=828)'s info (question, answer, answer type, tags) """ if not self.bot.d_handler.piazza_handler: raise BadArgs("Piazza hasn't been instantiated yet!") try: post = self.bot.d_handler.piazza_handler.get_post(postID) except InvalidPostID: raise BadArgs("Post not found.") if post: post_embed = self.create_post_embed(post) await ctx.send(embed=post_embed)
async def joinrole(self, ctx: commands.Context, *arg: str): """ `!joinrole` __`Adds a role to yourself`__ **Usage:** !joinrole [role name] **Examples:** `!joinrole Study Group` adds the Study Group role to yourself **Valid Roles:** Looking for Partners, Study Group, He/Him/His, She/Her/Hers, They/Them/Theirs, Ze/Zir/Zirs, notify """ await ctx.message.delete() # case where role name is space separated name = " ".join(arg) # Display help if given no argument if not name: raise BadArgs("", show_help=True) # make sure that you can't add roles like "prof" or "ta" valid_roles = ["Looking for Partners", "Study Group", "He/Him/His", "She/Her/Hers", "They/Them/Theirs", "Ze/Zir/Zirs", "notify"] # Grab the role that the user selected # Converters! this also makes aliases unnecessary try: role = await self.role_converter.convert(ctx, name) except commands.RoleNotFound: raise BadArgs("You can't add that role!", show_help=True) # Ensure that the author does not already have the role if role in ctx.author.roles: raise BadArgs("you already have that role!") # Special handling for roles that exist but can not be selected by a student if role.name not in valid_roles: self.add_instructor_role_counter += 1 if self.add_instructor_role_counter > 5: if self.add_instructor_role_counter == 42: if random.random() > 0.999: raise BadArgs("Congratulations, you found the secret message. IDEK how you did it, but good job. Still can't add the instructor role though. Bummer, I know.") elif self.add_instructor_role_counter == 69: if random.random() > 0.9999: raise BadArgs("nice.") raise BadArgs("You can't add that role, but if you try again, maybe something different will happen on the 42nd attempt") else: raise BadArgs("you cannot add an instructor/invalid role!", show_help=True) await ctx.author.add_roles(role) await ctx.send("role added!", delete_after=5)
async def shuffle(self, ctx: commands.Context): """ `!shuffle` __`Shuffle queue`__ **Usage:** !shuffle **Examples:** `!shuffle` shuffles queue """ if not self.voice_state.songs: raise BadArgs("Empty queue.") self.voice_state.songs.shuffle() await ctx.message.add_reaction("✅")
async def remove(self, ctx: commands.Context, index: int): """ `!remove` __`Remove song`__ **Aliases:** r **Usage:** !remove <index> **Examples:** `!remove 2` removes second song """ if not self.voice_state.songs: raise BadArgs("Empty queue.") self.voice_state.songs.remove(index - 1) await ctx.message.add_reaction("✅")
async def track(self, ctx: commands.Context, *course_ids: str): """ `!track <course IDs...>` Add the courses with given IDs to the list of courses being tracked. Note that you will only receive course updates in channels that you have typed `!live` in. """ self._add_guild(ctx.message.guild) c_handler = self._get_canvas_handler(ctx.message.guild) if not isinstance(c_handler, CanvasHandler): raise BadArgs("Canvas Handler doesn't exist.") c_handler.track_course(course_ids, self.bot.notify_unpublished) await self.send_canvas_track_msg(c_handler, ctx)
async def untrack(self, ctx: commands.Context, *course_ids: str): """ `!untrack <course IDs...>` Remove the courses with given IDs from the list of courses being tracked. """ c_handler = self._get_canvas_handler(ctx.message.guild) if not isinstance(c_handler, CanvasHandler): raise BadArgs("Canvas Handler doesn't exist.") c_handler.untrack_course(course_ids) if not c_handler.courses: self.bot.d_handler.canvas_handlers.remove(c_handler) await self.send_canvas_track_msg(c_handler, ctx)
async def annc(self, ctx: commands.Context, *args): """ `!annc ( | (-since (n-(hour|day|week|month|year)) | YYYY-MM-DD | YYYY-MM-DD-HH:MM:SS) | -all)` Argument can be left blank for sending announcements from 2 weeks ago to now. *Filter since announcement date:* `!annc -since` can be in time from now e.g.: `-since 4-hour` or all announcements after a certain date e.g.: `-since 2020-10-21` *All announcements:* `!annc -all` returns ALL announcements. """ c_handler = self._get_canvas_handler(ctx.message.guild) if not isinstance(c_handler, CanvasHandler): raise BadArgs("Canvas Handler doesn't exist.") if args and args[0].startswith("-since"): since = args[1] course_ids = args[2:] elif args and args[0].startswith("-all"): since = None course_ids = args[1:] else: since = "2-week" course_ids = args for data in c_handler.get_course_stream_ch(since, course_ids, CANVAS_API_URL, CANVAS_API_KEY): embed_var = discord.Embed(title=data[2], url=data[3], description=data[4], color=CANVAS_COLOR) embed_var.set_author(name=data[0], url=data[1]) embed_var.set_thumbnail(url=CANVAS_THUMBNAIL_URL) embed_var.add_field(name="Created at", value=data[5], inline=True) await ctx.send(embed=embed_var)
async def gtcycle(self, ctx, limit, *, txt): """ `!gtcycle` __`Google Translate Cycler`__ **Usage:** !gtcycle <number of languages | all> <text> **Examples:** `!gtcycle all hello!` cycles through all languages with input text "hello!" `!gtcycle 12 hello!` cycles through 12 languages with input text "hello!" """ lang_list = list(constants.LANGUAGES) random.shuffle(lang_list) if limit == "all": limit = len(lang_list) elif not (limit.isdecimal() and 1 < (limit := int(limit)) < len(constants.LANGUAGES)): raise BadArgs( f"Please send a positive integer number of languages less than {len(constants.LANGUAGES)} to cycle." )
async def votes(self, ctx: commands.Context): """ `!votes` __`Top votes for server icon`__ **Usage:** !votes **Examples:** `!votes` returns top 5 icons sorted by score """ async with ctx.channel.typing(): images = [] for c in ctx.guild.text_channels: if c.name == "course-events": channel = c break else: raise BadArgs("votes channel doesn't exist.") async for message in channel.history(): if message.attachments or message.embeds: count = 0 for reaction in message.reactions: if reaction.emoji == "⬆️": count += reaction.count - ( message.author in await reaction.users().flatten()) images.append([message.attachments[0].url, count]) images = sorted(images, key=lambda image: image[1], reverse=True)[:5] for image in images: embed = discord.Embed(colour=random.randint(0, 0xFFFFFF)) embed.add_field(name="Score", value=image[1], inline=True) embed.set_thumbnail(url=image[0]) await ctx.send(embed=embed)
async def help(self, ctx: commands.Context, *arg: str): """ `!help` __`Returns list of commands or usage of command`__ **Usage:** !help [optional cmd] **Examples:** `!help` [embed] """ if not arg: embed = discord.Embed(title="CS221 Bot", description="Commands:", colour=random.randint(0, 0xFFFFFF), timestamp=datetime.utcnow()) embed.add_field( name=f"❗ Current Prefix: `{self.bot.command_prefix}`", value="\u200b", inline=False) for k, v in sorted(self.bot.cogs.items(), key=lambda kv: kv[0]): embed.add_field(name=k, value=" ".join(f"`{i}`" for i in v.get_commands() if not i.hidden), inline=False) embed.set_thumbnail(url=self.bot.user.avatar_url) embed.set_footer(text=f"Requested by {ctx.author.display_name}", icon_url=str(ctx.author.avatar_url)) await ctx.send(embed=embed) else: help_command = arg[0] comm = self.bot.get_command(help_command) if not comm or not comm.help or comm.hidden: raise BadArgs("That command doesn't exist.") await ctx.send(comm.help)
async def play(self, ctx: commands.Context, *, search: str): """ `!play` __`Play song`__ **Aliases:** p **Usage:** !play <url> **Examples:** `!play https://www.youtube.com/watch?v=dQw4w9WgXcQ` plays Never Gonna Give You Up """ if not re.match(r"https://(www\.youtube|soundcloud)\.com", search, flags=re.IGNORECASE): raise BadArgs("Only links allowed.") if not ctx.voice_client: destination = ctx.author.voice.channel if self.voice_state.voice: await self.voice_state.voice.move_to(destination) return self.voice_state.voice = await destination.connect() async with ctx.typing(): try: source = await YTDLSource.create_source(ctx, search, loop=self.bot.loop) except YTDLError as e: await ctx.send( f"An error occurred while processing this request: {e}", delete_after=5) else: song = Song(source) await self.voice_state.songs.put(song) await ctx.send(f"Enqueued {source}")
async def live(self, ctx: commands.Context): """ `!live` Enables course tracking for the channel the command is invoked in. """ c_handler = self._get_canvas_handler(ctx.message.guild) if not isinstance(c_handler, CanvasHandler): raise BadArgs("Canvas Handler doesn't exist.") if ctx.message.channel not in c_handler.live_channels: c_handler.live_channels.append(ctx.message.channel) for course in c_handler.courses: modules_file = f"{util.canvas_handler.COURSES_DIRECTORY}/{course.id}/modules.txt" watchers_file = f"{util.canvas_handler.COURSES_DIRECTORY}/{course.id}/watchers.txt" CanvasHandler.store_channels_in_file([ctx.message.channel], watchers_file) create_file_if_not_exists(modules_file) # Here, we will only download modules if modules_file is empty. if os.stat(modules_file).st_size == 0: CanvasHandler.download_modules(course, self.bot.notify_unpublished) self.canvas_dict[str(ctx.message.guild.id)]["live_channels"] = [ channel.id for channel in c_handler.live_channels ] writeJSON(self.canvas_dict, "data/canvas.json") await ctx.send("Added channel to live tracking.") else: await ctx.send("Channel already live tracking.")
async def bst(self, ctx): """ `!bst` __`Binary Search Tree analysis tool`__ **Usage:** !bst <node> [node] [...] **Examples:** `!bst 2 1 3` displays a BST in ASCII and PNG form with root node 2 and leaves 1, 3 `!bst 4 5 6` displays a BST in ASCII and PNG form with root node 4, parent node 5 and leaf 6 Launching the command activates a 60 second window during which additional unprefixed commands can be called: `pre` displays pre-order traversal of the tree `in` displays in-order traversal of the tree `level` displays level-order traversal of the tree `post` displays post-order traversal of the tree `about` displays characteristics of the tree `pause` stops the 60 second countdown timer `unpause` starts the 60 second countdown timer `show` displays the ASCII and PNG representations of the tree again `exit` exits the window `insert <node> [node] [...]` inserts nodes into the tree **Example:** `insert 5 7 6` inserts nodes 5, 7 and 6, in that order `delete <node> [node] [...]` deletes nodes from the tree **Example:** `delete 7 8 9` deletes nodes 7, 8 and 9, in that order """ numbers = [] for num in ctx.message.content[5:].replace(",", "").split(): if re.fullmatch(r"[+-]?((\d+(\.\d*)?)|(\.\d+))", num): try: numbers.append(int(num)) except ValueError: numbers.append(float(num)) else: raise BadArgs("Please provide valid numbers for the tree.") if not numbers: raise BadArgs("Please provide some numbers for the tree.", show_help=True) root = Node(numbers[0]) nodes = [root] highlighted = [] def insert(subroot, num, highlight=False): if highlight: highlighted.append(subroot.val) if num < subroot.val: if not subroot.left: node = Node(num) subroot.left = node if highlight: highlighted.append(num) nodes.append(node) else: insert(subroot.left, num, highlight) else: if not subroot.right: node = Node(num) subroot.right = node if highlight: highlighted.append(num) nodes.append(node) else: insert(subroot.right, num, highlight) def delete(subroot, num): if subroot: if subroot.val == num: if subroot.left is not None and subroot.right is not None: parent = subroot predecessor = subroot.left while predecessor.right is not None: parent = predecessor predecessor = predecessor.right if parent.right == predecessor: parent.right = predecessor.left else: parent.left = predecessor.left predecessor.left = subroot.left predecessor.right = subroot.right ret = predecessor else: if subroot.left is not None: ret = subroot.left else: ret = subroot.right nodes.remove(subroot) del subroot return ret else: if subroot.val > num: if subroot.left: subroot.left = delete(subroot.left, num) else: if subroot.right: subroot.right = delete(subroot.right, num) return subroot def get_node(num): for node in nodes: if node.val == num: return node return None for num in numbers[1:]: if not get_node(num): insert(root, num) timeout = 60 display = True filex = None async def draw_bst(highlighted, root): entries = list( filter(lambda x: x[0] is not None, [[ b, i ] for b, i in zip(root.values, range(1, len(root.values) + 1))])) levels = root.height + 1 fsize = max(10, 60 - 7 * root.height) font = ImageFont.truetype("boxfont_round.ttf", fsize) radius = max(10, 100 - 10 * root.height) width = 2 * radius * (2**(root.height + 1)) height = 2 * radius * (root.height + 2) basey = height // (levels + 1) smallest = [math.inf, 1] for entry in entries: if entry[0] < smallest[0]: smallest = entry reflevel = math.floor(math.log(smallest[1], 2)) basex = width // (2**reflevel + 1) refx = int(basex - 2 * radius) if basex != width // 2: refx = 0 offset = 0 else: offset = radius image = Image.new("RGBA", (width - refx - offset, height), (255, 255, 255, 255)) layer = Image.new("RGBA", (width - refx - offset, height), (0, 0, 0, 0)) drawing = ImageDraw.Draw(image) drawing2 = ImageDraw.Draw(layer) currentlevel = 2 x = width // 2 y = basey * currentlevel if root.val in highlighted: col = (0, 128, 128, 255) else: col = (0, 0, 255, 255) drawing2.ellipse([(x - radius - refx, basey - radius), (x + radius - refx, basey + radius)], fill=col, outline=(0, 0, 0, 255)) ln = drawing2.textsize(str(root.val), font=font)[0] / 2 drawing2.text((x - refx - fsize // 2 - ln, basey - fsize // 2), str(root.val), fill=(255, 168, 0, 255), font=font) for entry in entries[1:]: await asyncio.sleep(0) if math.floor(math.log(entry[1], 2)) > currentlevel - 1: currentlevel += 1 y = basey * currentlevel multiplier = entry[1] - 2**(currentlevel - 1) + 1 basex = width // (2**(currentlevel - 1) + 1) x = basex * multiplier prevx = width // (2**(currentlevel - 2) + 1) prevx *= (multiplier + 1) // 2 if entry[0] in highlighted: col = (0, 128, 128, 255) linecol = col else: col = (0, 0, 255, 255) linecol = (0, 0, 0, 255) drawing.line([(prevx - refx, basey * (currentlevel - 1)), (x - refx, y)], fill=linecol, width=7) drawing2.ellipse([(x - radius - refx, y - radius), (x + radius - refx, y + radius)], fill=col, outline=(0, 0, 0, 255)) ln = drawing2.textsize(str(entry[0]), font=font)[0] / 2 drawing2.text((x - refx - fsize // 2 - ln, y - fsize // 2), str(entry[0]), fill=(255, 168, 0, 255), font=font) image.alpha_composite(layer) filex = BytesIO() image.save(filex, "PNG", optimize=True) filex.seek(0) return filex while True: if root.height <= 8: filex = await draw_bst(highlighted, root) highlighted = [] if display: text = f"```{root}\n```" await ctx.send( text, file=discord.File(filex, "bst.png") if filex else None) display = False def check(m): return m.channel.id == ctx.channel.id and m.author.id == ctx.author.id try: message = await self.bot.wait_for("message", timeout=timeout, check=check) except asyncio.exceptions.TimeoutError: return command = message.content.replace(",", "").replace("!", "").lower() timeout = 60 if command.startswith("level"): await ctx.send("Level-Order Traversal:\n**" + " ".join([str(n.val) for n in root.levelorder]) + "**") elif command.startswith("pre"): await ctx.send("Pre-Order Traversal:\n**" + " ".join([str(n.val) for n in root.preorder]) + "**") elif command.startswith("post"): await ctx.send("Post-Order Traversal:\n**" + " ".join([str(n.val) for n in root.postorder]) + "**") elif command.startswith("in") and not command.startswith("ins"): await ctx.send("In-Order Traversal:\n**" + " ".join([str(n.val) for n in root.inorder]) + "**") elif command.startswith("about"): embed = discord.Embed(title="Binary Search Tree Info", description="> " + text.replace("\n", "\n> "), color=random.randint(0, 0xffffff)) embed.add_field(name="Height:", value=str(root.height)) embed.add_field(name="Balanced?", value=str(root.is_balanced)) embed.add_field(name="Complete?", value=str(root.is_complete)) embed.add_field(name="Full?", value=str(root.is_strict)) embed.add_field(name="Perfect?", value=str(root.is_perfect)) embed.add_field(name="Number of leaves:", value=str(root.leaf_count)) embed.add_field(name="Max Leaf Depth:", value=str(root.max_leaf_depth)) embed.add_field(name="Min Leaf Depth:", value=str(root.min_leaf_depth)) embed.add_field(name="Max Node Value:", value=str(root.max_node_value)) embed.add_field(name="Min Node Value:", value=str(root.min_node_value)) embed.add_field(name="Entries:", value=str(root.size)) embed.add_field(name="Pre-Order Traversal:", value=" ".join( [str(n.val) for n in root.preorder])) embed.add_field(name="In-Order Traversal:", value=" ".join( [str(n.val) for n in root.inorder])) embed.add_field(name="Level-Order Traversal:", value=" ".join( [str(n.val) for n in root.levelorder])) embed.add_field(name="Post-Order Traversal:", value=" ".join( [str(n.val) for n in root.postorder])) if root.left: embed.add_field(name="In-Order Predecessor:", value=max( filter(lambda x: x is not None, root.left.values))) if root.right: embed.add_field(name="In-Order Successor:", value=min( filter(lambda x: x is not None, root.right.values))) filex.seek(0) await ctx.send(embed=embed, file=discord.File(filex, "bst.png")) elif command.startswith("pause"): timeout = 86400 await ctx.send("Timeout paused.") elif command.startswith("unpause"): timeout = 60 await ctx.send("Timeout reset to 60 seconds.") elif command.startswith("show"): display = True elif command.startswith("insert"): add = [] for entry in command[7:].split(): if re.fullmatch(r"[+-]?((\d+(\.\d*)?)|(\.\d+))", entry): try: num = int(entry) except ValueError: num = float(entry) else: continue add.append(str(num)) if not get_node(num): insert(root, num, highlight=True) await ctx.send(f"Inserted {','.join(add)}.") display = True elif command.startswith("delete"): remove = [] for entry in command[7:].split(): try: num = float(entry) except Exception: continue if root.size == 1: await ctx.send( "Tree has reached one node in size. Stopping deletions." ) break if math.modf(num)[0] == 0: num = int(round(num)) if not get_node(num): continue remove.append(str(num)) root = delete(root, num) await ctx.send(f"Deleted {','.join(remove)}.") display = True elif command.startswith("exit"): return await ctx.send("Exiting.") elif command.startswith("bst"): return
for url in options_dict.values(): if mimetypes.guess_type(url)[0] and mimetypes.guess_type( url)[0].startswith("image"): filex = BytesIO(requests.get(url).content) filex.seek(0) files.append(discord.File(filex, filename=url)) return await ctx.send(output, files=files) if id_: return await ctx.send( "There's an active poll in this channel already.") if len(options) <= 1: raise BadArgs("Please enter more than one option to poll.", show_help=True) elif len(options) > 20: raise BadArgs("Please limit to 10 options.") elif len(options) == 2 and options[0] == "yes" and options[1] == "no": reactions = ["✅", "❌"] else: reactions = tuple(chr(127462 + i) for i in range(26)) description = [] for x, option in enumerate(options): description += f"\n{reactions[x]}: {option}" embed = discord.Embed(title=question, description="".join(description)) files = []
async def poll(self, ctx: commands.Context): """ `!poll` __`Poll generator`__ **Usage:** !poll <question | check | end> | <option> | <option> | [option] | [...] **Examples:** `!poll poll | yee | nah` generates poll titled "poll" with options "yee" and "nah" `!poll check` returns content of last poll `!poll end` ends current poll in channel and returns results """ poll_list = tuple(map(str.strip, ctx.message.content[6:].split("|"))) question = poll_list[0] options = poll_list[1:] id_ = self.poll_dict.get(str(ctx.channel.id), "") if question in ("check", "end"): if end := (question == "end"): del self.poll_dict[str(ctx.channel.id)] write_json(self.poll_dict, "data/poll.json") if not id_: raise BadArgs("No active poll found.") try: poll_message = await ctx.channel.fetch_message(id_) except discord.NotFound: raise BadArgs( "Looks like someone deleted the poll, or there is no active poll." ) embed = poll_message.embeds[0] unformatted_options = [ x.strip().split(": ") for x in embed.description.split("\n") ] options_dict = {} for x in unformatted_options: options_dict[x[0]] = x[1] tally = {x: 0 for x in options_dict} for reaction in poll_message.reactions: if reaction.emoji in options_dict: async for reactor in reaction.users(): if reactor.id != self.bot.user.id: tally[reaction.emoji] += 1 output = f"{'Final' if end else 'Current'} results of the poll **\"{embed.title}\"**\nLink: {poll_message.jump_url}\n```" max_len = max(map(len, options_dict.values())) for key in tally: output += f"{options_dict[key].ljust(max_len)}: " + \ f"{('👑' * tally[key]).replace('👑', '▓', ((tally[key] - 1) or 1) - 1) if tally[key] == max(tally.values()) else '░' * tally[key]}".ljust(max(tally.values())).replace('👑👑', '👑') + \ f" ({tally[key]} votes, {round(tally[key] / sum(tally.values()) * 100, 2) if sum(tally.values()) else 0}%)\n\n" output += "```" files = [] for url in options_dict.values(): if mimetypes.guess_type(url)[0] and mimetypes.guess_type( url)[0].startswith("image"): filex = BytesIO(requests.get(url).content) filex.seek(0) files.append(discord.File(filex, filename=url)) return await ctx.send(output, files=files)
class Commands(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot self.add_instructor_role_counter = 0 self.bot.d_handler = DiscordHandler() self.role_converter = CustomRoleConverter() if not isfile(POLL_FILE): create_file_if_not_exists(POLL_FILE) write_json({}, POLL_FILE) self.poll_dict = read_json(POLL_FILE) for channel in filter(lambda ch: not self.bot.get_channel(int(ch)), list(self.poll_dict)): del self.poll_dict[channel] for channel in (c for g in self.bot.guilds for c in g.text_channels if str(c.id) not in self.poll_dict): self.poll_dict.update({str(channel.id): ""}) write_json(self.poll_dict, POLL_FILE) @commands.command() @commands.cooldown(1, 5, commands.BucketType.user) async def emojify(self, ctx: commands.Context): """ `!emojify` __`Emoji text generator`__ **Usage:** !emojify <text> **Examples:** `!emojify hello` prints "hello" with emoji `!emojify b` prints b with emoji" """ mapping = { "A": "🇦", "B": "🅱", "C": "🇨", "D": "🇩", "E": "🇪", "F": "🇫", "G": "🇬", "H": "🇭", "I": "🇮", "J": "🇯", "K": "🇰", "L": "🇱", "M": "🇲", "N": "🇳", "O": "🇴", "P": "🇵", "Q": "🇶", "R": "🇷", "S": "🇸", "T": "🇹", "U": "🇺", "V": "🇻", "W": "🇼", "X": "🇽", "Y": "🇾", "Z": "🇿", "0": "0️⃣", "1": "1️⃣", "2": "2️⃣", "3": "3️⃣", "4": "4️⃣", "5": "5️⃣", "6": "6️⃣", "7": "7️⃣", "8": "8️⃣", "9": "9️⃣" } text = ctx.message.content[9:].upper() output = "".join( mapping[i] + (" " if i in string.ascii_uppercase else "") if i in mapping else i for i in text) await ctx.send(output) @commands.command() @commands.cooldown(1, 5, commands.BucketType.user) async def join(self, ctx: commands.Context, *arg: str): """ `!join` __`Adds a role to yourself`__ **Usage:** !join [role name] **Examples:** `!join Study Group` adds the Study Group role to yourself **Valid Roles:** Looking for Partners, Study Group, He/Him/His, She/Her/Hers, They/Them/Theirs, Ze/Zir/Zirs, notify """ await ctx.message.delete() # case where role name is space separated name = " ".join(arg) # Display help if given no argument if not name: raise BadArgs("", show_help=True) # make sure that you can't add roles like "prof" or "ta" valid_roles = [ "Looking for Partners", "Study Group", "He/Him/His", "She/Her/Hers", "They/Them/Theirs", "Ze/Zir/Zirs", "notify" ] # Grab the role that the user selected # Converters! this also makes aliases unnecessary try: role = await self.role_converter.convert(ctx, name) except commands.RoleNotFound: raise BadArgs("You can't add that role!", show_help=True) # Ensure that the author does not already have the role if role in ctx.author.roles: raise BadArgs("you already have that role!") # Special handling for roles that exist but can not be selected by a student if role.name not in valid_roles: self.add_instructor_role_counter += 1 if self.add_instructor_role_counter > 5: if self.add_instructor_role_counter == 42: if random.random() > 0.999: raise BadArgs( "Congratulations, you found the secret message. IDEK how you did it, but good job. Still can't add the instructor role though. Bummer, I know." ) elif self.add_instructor_role_counter == 69: if random.random() > 0.9999: raise BadArgs("nice.") raise BadArgs( "You can't add that role, but if you try again, maybe something different will happen on the 42nd attempt" ) else: raise BadArgs("you cannot add an instructor/invalid role!", show_help=True) await ctx.author.add_roles(role) await ctx.send("role added!", delete_after=5) @commands.command() @commands.cooldown(1, 10, commands.BucketType.user) async def latex(self, ctx: commands.Context, *args: str): """ `!latex` __`LaTeX equation render`__ **Usage:** !latex <equation> **Examples:** `!latex \\frac{a}{b}` [img] """ formula = " ".join(args).strip("\n ") if sm := re.match(r"```(latex|tex)", formula): formula = formula[6 if sm.group(1) == "tex" else 8:] formula = formula.strip("`") body = { "formula": formula, "fsize": r"30px", "fcolor": r"FFFFFF", "mode": r"0", "out": r"1", "remhost": r"quicklatex.com", "preamble": r"\usepackage{amsmath}\usepackage{amsfonts}\usepackage{amssymb}", "rnd": str(random.random() * 100) } try: img = requests.post("https://www.quicklatex.com/latex3.f", data=body, timeout=10) except (requests.ConnectionError, requests.HTTPError, requests.TooManyRedirects, requests.Timeout): raise BadArgs("Render timed out.") if img.status_code != 200: raise BadArgs("Something done goofed. Maybe check your syntax?") if img.text.startswith("0"): await ctx.send(file=discord.File( BytesIO(requests.get(img.text.split()[1]).content), "latex.png")) else: await ctx.send(" ".join(img.text.split()[5:]), delete_after=5)
async def purge(self, ctx: commands.Context, amount: int, *arg: str): """ `!purge` __`purges all messages satisfying conditions from the last specified number of messages in channel.`__ Usage: !purge <amount of messages to look through> [user | string <containing string> | reactions | type] **Options:** `user` - Only deletes messages sent by this user `string` - Will delete messages containing following string `reactions` - Will remove all reactions from messages `type` - One of valid types **Valid Types:** `text` - Is text only? (ignores image or embeds) `links` - Contains links? `bots` - Was send by bots? `images` - Contains images? `embeds` - Contains embeds? `mentions` - Contains user, role or everyone/here mentions? **Examples:** `!purge 100` deletes last 100 messages in channel `!purge 50 abc#1234` deletes all messages sent by abc#1234 in last 50 messages `!purge 30 string asdf ghjk` deletes all messages containing "asdf ghjk" in last 30 messages """ await ctx.message.delete() if not ctx.author.guild_permissions.manage_messages: return await ctx.send( "Oops! It looks like you don't have the permission to purge.", delete_after=5) if amount > 100: raise BadArgs("Please enter a smaller number to purge.") if not arg: total_messages = await ctx.channel.purge(limit=amount) await ctx.send( f"**{len(total_messages)}** message{'s' if len(total_messages) > 1 else ''} cleared.", delete_after=5) elif arg[0] == "reactions": messages = await ctx.channel.history(limit=amount).flatten() for i in messages: if i.reactions: await i.clear_reactions() await ctx.send( f"Reactions removed from the last {'' if amount == 1 else '**' + str(amount) + '**'} message{'s' if amount > 1 else ''}.", delete_after=5) elif arg[0] == "text": total_messages = await ctx.channel.purge( limit=amount, check=lambda m: not m.embeds and not m.attachments) await ctx.send( f"**{len(total_messages)}** text message{'s' if len(total_messages) > 1 else ''} purged." ) elif arg[0] == "bots": total_messages = await ctx.channel.purge( limit=amount, check=lambda m: m.author.bot) await ctx.send( f"**{len(total_messages)}** bot message{'s' if len(total_messages) > 1 else ''} purged.", delete_after=5) elif arg[0] == "images": total_messages = await ctx.channel.purge( limit=amount, check=lambda m: m.attachments) await ctx.send( f"**{len(total_messages)}** image message{'s' if len(total_messages) > 1 else ''} purged.", delete_after=5) elif arg[0] == "embeds": total_messages = await ctx.channel.purge(limit=amount, check=lambda m: m.embeds) await ctx.send( f"**{len(total_messages)}** embed message{'s' if len(total_messages) > 1 else ''} purged.", delete_after=5) elif arg[0] == "mentions": total_messages = await ctx.channel.purge( limit=amount, check=lambda m: m.mentions) await ctx.send( f"**{len(total_messages)}** mention message{'s' if len(total_messages) > 1 else ''} purged.", delete_after=5) elif arg[0] == "links": total_messages = await ctx.channel.purge( limit=amount, check=lambda m: bool( re.search(r"https?://[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+", m. content))) await ctx.send( f"**{len(total_messages)}** link message{'s' if len(total_messages) > 1 else ''} purged.", delete_after=5) elif arg[0] == "string": total_messages = await ctx.channel.purge( limit=amount, check=lambda m: " ".join(arg[1:]) in m.content) await ctx.send( f"**{len(total_messages)}** message{'s' if len(total_messages) > 1 else ''} containing \"{' '.join(arg[1:])}\" purged." ) else: try: user = await MemberConverter().convert(ctx, " ".join(arg)) except BadArgument: return await ctx.send("That user doesn't exist.", delete_after=5) total_messages = await ctx.channel.purge( limit=amount, check=lambda m: m.author == user) await ctx.send( f"**{len(total_messages)}** message{'s' if len(total_messages) > 1 else ''} from {user.display_name} purged.", delete_after=5)