Example #1
0
    async def assignment_reminder(self):
        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

                    writeJSON(self.canvas_dict, "data/canvas.json")

            await asyncio.sleep(30)
Example #2
0
    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.")
Example #3
0
    async def untrack_servers(self, ctx):
        """
        `!untrackservers` __`Untracks CS server live status updates`__

        Causes the bot to stop sending CS server updates to the channel where
        this command is invoked. The bot also deletes the live status message
        in the channel, if that message exists.
        """

        if ctx.channel.id in self.server_trackers_dict:
            live_msg_id = self.server_trackers_dict.pop(ctx.channel.id, None)
            writeJSON(self.server_trackers_dict, SERVER_TRACKERS_FILE)

            if live_msg_id is not None:
                live_msg = ctx.channel.get_partial_message(live_msg_id)

                try:
                    await live_msg.delete()
                except discord.NotFound:
                    # The message was already deleted
                    pass

            await ctx.send(
                "This channel will no longer receive live CS server status updates.",
                delete_after=5)
        else:
            await ctx.send(
                "This channel is not receiving live CS server status updates.",
                delete_after=5)
Example #4
0
    async def puntrack(self, ctx, *cid):
        """
        `!puntrack` __`channel id`__

        **Usage:** !puntrack [channel id]

        **Examples:**
        `!puntrack 747259140908384386` removes CPSC221 server's #bot-commands channel id to the Piazza instance's list of tracked channels
        `!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
        """

        if not cid:
            cid = ctx.message.channel.id
        else:
            cid = int(cid[0])

        self.bot.d_handler.piazza_handler.remove_channel(cid)
        self.piazza_dict["channels"] = list(
            self.bot.d_handler.piazza_handler.channels)
        writeJSON(self.piazza_dict, "data/piazza.json")
        await ctx.send("Channel removed from tracking!")
Example #5
0
    async def pinit(self, ctx, name, pid):
        """
        `!pinit` _ `course name` _ _ `piazza id` _

        **Usage:** !pinit <course name> <piazza id>

        **Examples:**
        `!pinit CPSC221 ke1ukp9g4xx6oi` 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 defaults to None 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["course_name"] = name
        self.piazza_dict["piazza_id"] = pid
        self.piazza_dict["guild_id"] = ctx.guild.id
        writeJSON(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)
Example #6
0
    def __init__(self, bot):
        self.bot = bot

        if not isfile(CANVAS_FILE):
            create_file_if_not_exists(CANVAS_FILE)
            writeJSON({}, CANVAS_FILE)

        self.canvas_dict = readJSON(CANVAS_FILE)
Example #7
0
    def __init__(self, bot):
        self.bot = bot

        if not isfile(PIAZZA_FILE):
            create_file_if_not_exists(PIAZZA_FILE)
            writeJSON({}, PIAZZA_FILE)

        self.piazza_dict = readJSON(PIAZZA_FILE)
Example #8
0
 def _add_guild(self, guild: discord.Guild):
     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": {}
         }
         writeJSON(self.canvas_dict, "data/canvas.json")
Example #9
0
    def __init__(self, bot):
        self.bot = bot

        if not isfile(SERVER_TRACKERS_FILE):
            create_file_if_not_exists(SERVER_TRACKERS_FILE)
            writeJSON({}, SERVER_TRACKERS_FILE)

        with open(SERVER_TRACKERS_FILE, "r") as f:
            # Maps channel ID to live message ID
            # All keys in the JSON are ints stored as strings. The hook function turns those keys into ints.
            self.server_trackers_dict: Dict[int, Optional[int]] = json.load(
                f, object_hook=lambda dct: {int(key): dct[key]
                                            for key in dct})
Example #10
0
    async def send_canvas_track_msg(self, c_handler: CanvasHandler,
                                    ctx: commands.Context):
        """
        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

        writeJSON(self.canvas_dict, "data/canvas.json")

        embed_var = self._get_tracking_courses(c_handler, CANVAS_API_URL)
        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)
Example #11
0
    async def track_servers(self, ctx):
        """
        `!trackservers` __`Get CS server status updates live`__

        Causes the bot to periodically check the statuses of the remote CS servers,
        updating a live status message in the channel where this command is invoked.
        """

        if ctx.channel.id not in self.server_trackers_dict:
            self.server_trackers_dict[ctx.channel.id] = None
            writeJSON(self.server_trackers_dict, SERVER_TRACKERS_FILE)
            await ctx.send(
                "This channel will now receive live CS server status updates.",
                delete_after=5)
        else:
            await ctx.send(
                "This channel is already receiving live CS server status updates.",
                delete_after=5)
Example #12
0
    def __init__(self, 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)
            writeJSON({}, POLL_FILE)

        self.poll_dict = readJSON(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): ""})

        writeJSON(self.poll_dict, POLL_FILE)
Example #13
0
    async def update_server_statuses(self):
        """
        Gets the statuses of the CS servers and updates a live status message in each channel
        that is tracking the servers. We will call these channels "tracking channels".

        If there is no live status message in a tracking channel, the bot sends one. Otherwise,
        the bot simply edits the existing status message.

        If a tracking channel has been deleted, the bot will no longer attempt to send status
        updates to that channel.
        """

        message = await self.get_server_statuses()
        deleted_channel_ids = []

        for channel_id, msg_id in self.server_trackers_dict.items():
            channel = self.bot.get_channel(channel_id)

            if channel is not None:
                if msg_id is not None:
                    live_msg = channel.get_partial_message(msg_id)

                    try:
                        await live_msg.edit(content=message)
                    except discord.NotFound:
                        live_msg = await channel.send(message)
                        self.server_trackers_dict[channel_id] = live_msg.id
                else:
                    live_msg = await channel.send(message)
                    self.server_trackers_dict[channel_id] = live_msg.id
            else:
                deleted_channel_ids.append(channel_id)

        for channel_id in deleted_channel_ids:
            del self.server_trackers_dict[channel_id]

        writeJSON(self.server_trackers_dict, SERVER_TRACKERS_FILE)
Example #14
0
    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.")
Example #15
0
    async def poll(self, ctx):
        """
        `!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)]
                writeJSON(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)
Example #16
0
        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
        writeJSON(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):
        """
        `!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