async def assignment_reminder(self) -> None: while True: for ch in filter(operator.attrgetter("live_channels"), self.bot.d_handler.canvas_handlers): notify_role = next( (r for r in ch.guild.roles if r.name.lower() == "notify"), None) for c in ch.courses: for time in ("week", "day"): data_list = ch.get_assignments(f"1-{time}", (str(c.id), ), CANVAS_API_URL) if time == "week": recorded_ass_ids = ch.due_week[str(c.id)] else: recorded_ass_ids = ch.due_day[str(c.id)] ass_ids = await self._assignment_sender( ch, data_list, recorded_ass_ids, notify_role, time) if time == "week": ch.due_week[str(c.id)] = ass_ids else: ch.due_day[str(c.id)] = ass_ids self.canvas_dict[str(ch.guild.id)][f"due_{time}"][str( c.id)] = ass_ids write_json(self.canvas_dict, "data/canvas.json") await asyncio.sleep(30)
async def pinit(self, ctx: commands.Context, name: str, pid: str): """ `!pinit` _ `course name` _ _ `piazza id` _ **Usage:** !pinit <course name> <piazza id> **Examples:** `!pinit CPSC221 abcdef1234` creates a CPSC221 Piazza instance for the server *Only usable by TAs and Profs """ self.bot.d_handler.piazza_handler = PiazzaHandler( name, pid, PIAZZA_EMAIL, PIAZZA_PASSWORD, ctx.guild) # dict.get default to empty list so KeyError is never thrown for channel in self.piazza_dict.get("channels", []): self.bot.d_handler.piazza_handler.add_channel(channel) self.piazza_dict["channels"] = [ctx.channel.id] self.piazza_dict["course_name"] = name self.piazza_dict["piazza_id"] = pid self.piazza_dict["guild_id"] = ctx.guild.id write_json(self.piazza_dict, "data/piazza.json") response = f"Piazza instance created!\nName: {name}\nPiazza ID: {pid}\n" response += "If the above doesn't look right, please use `!pinit` again with the correct arguments" await ctx.send(response)
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 ] write_json(self.canvas_dict, "data/canvas.json") for course in c_handler.courses: watchers_file = f"{util.canvas_handler.COURSES_DIRECTORY}/{course.id}/watchers.txt" c_handler.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.")
def __init__(self, bot: commands.Bot): self.bot = bot if not isfile(PIAZZA_FILE): create_file_if_not_exists(PIAZZA_FILE) write_json({}, PIAZZA_FILE) self.piazza_dict = read_json(PIAZZA_FILE)
def __init__(self, bot: commands.Bot): self.bot = bot if not isfile(CANVAS_FILE): create_file_if_not_exists(CANVAS_FILE) write_json({}, CANVAS_FILE) self.canvas_dict = read_json(CANVAS_FILE)
def _add_guild(self, guild: discord.Guild) -> None: if guild not in (ch.guild for ch in self.bot.d_handler.canvas_handlers): self.bot.d_handler.canvas_handlers.append( CanvasHandler(CANVAS_API_URL, CANVAS_API_KEY, guild)) self.canvas_dict[str(guild.id)] = { "courses": [], "live_channels": [], "due_week": {}, "due_day": {} } write_json(self.canvas_dict, "data/canvas.json")
async def send_canvas_track_msg(self, c_handler: CanvasHandler, ctx: commands.Context) -> None: """ Sends an embed to ctx that lists the Canvas courses being tracked by c_handler. """ guild_dict = self.canvas_dict[str(ctx.message.guild.id)] guild_dict["courses"] = [str(c.id) for c in c_handler.courses] guild_dict["due_week"] = c_handler.due_week guild_dict["due_day"] = c_handler.due_day write_json(self.canvas_dict, "data/canvas.json") embed_var = self._get_tracking_courses(c_handler) embed_var.set_footer(text=f"Requested by {ctx.author.display_name}", icon_url=str(ctx.author.avatar_url)) await ctx.send(embed=embed_var)
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)
async def puntrack(self, ctx: commands.Context): """ `!puntrack` __`Untracks Piazza posts in channel`__ **Usage:** !puntrack **Examples:** `!puntrack` removes the current channel's id to the Piazza instance's list of channels The channels removed through `!puntrack` are where send_pupdate and track_inotes send their responses. *Only usable by TAs and Profs """ cid = ctx.message.channel.id self.bot.d_handler.piazza_handler.remove_channel(cid) self.piazza_dict[ "channels"] = self.bot.d_handler.piazza_handler.channels write_json(self.piazza_dict, "data/piazza.json") await ctx.send("Channel removed from tracking!")
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" c_handler.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: c_handler.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 ] write_json(self.canvas_dict, "data/canvas.json") await ctx.send("Added channel to live tracking.") else: await ctx.send("Channel already live tracking.")
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)
files = [] for url in options: 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)) react_message = await ctx.send(embed=embed, files=files) for reaction in reactions[:len(options)]: await react_message.add_reaction(reaction) self.poll_dict[str(ctx.channel.id)] = react_message.id write_json(self.poll_dict, "data/poll.json") @commands.command() @commands.cooldown(1, 5, commands.BucketType.user) 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