Beispiel #1
0
    def store_channels_in_file(text_channels: List[discord.TextChannel],
                               file_path: str):
        """
        For each text channel provided, we add its id to the file with given path if the file does
        not already contain the id.
        """

        if text_channels:
            create_file.create_file_if_not_exists(file_path)

            with open(file_path, "r") as f:
                existing_ids = f.readlines()

            ids_to_add = set(
                map(lambda channel: str(channel.id) + "\n", text_channels))

            with open(file_path, "w") as f:
                for channel_id in existing_ids:
                    if channel_id in ids_to_add:
                        ids_to_add.remove(channel_id)

                    f.write(channel_id)

                for channel_id in ids_to_add:
                    f.write(channel_id)
Beispiel #2
0
def startup():
    files = ["data/poll.json", "data/pl.json"]

    for f in files:
        if not isfile(f):
            create_file_if_not_exists(f)
            bot.writeJSON({}, f)

    for f in ["data/tomorrow.json"]:
        if not isfile(f):
            create_file_if_not_exists(f)
            bot.writeJSON([], f)

    bot.poll_dict = bot.loadJSON("data/poll.json")
    bot.pl_dict = defaultdict(list, bot.loadJSON("data/pl.json"))
    bot.due_tomorrow = bot.loadJSON("data/tomorrow.json")

    for channel in filter(lambda ch: not bot.get_channel(int(ch)),
                          list(bot.poll_dict)):
        del bot.poll_dict[channel]

    for channel in (c for g in bot.guilds for c in g.text_channels
                    if str(c.id) not in bot.poll_dict):
        bot.poll_dict.update({str(channel.id): ""})

    bot.writeJSON(dict(bot.poll_dict), "data/poll.json")
Beispiel #3
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)
Beispiel #4
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)
Beispiel #5
0
    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)
Beispiel #6
0
    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)
Beispiel #7
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})
Beispiel #8
0
    def track_course(self, course_ids_str: Tuple[str],
                     get_unpublished_modules: bool):
        """
        Cause this CanvasHandler to start tracking the courses with given IDs.

        For each course, if the bot is tracking the course for the first time,
        the course's modules will be downloaded from Canvas and saved in the course's
        directory (located in /data/courses/). If `get_unpublished_modules` is `True`, and
        we have access to unpublished modules for the course, then we will save both published and
        unpublished modules to file. Otherwise, we will only save published modules.

        Parameters
        ----------
        course_ids_str : `Tuple[str]`
            Tuple of course ids

        get_unpublished_modules: `bool`
            True if we should attempt to store unpublished modules for the courses in `course_ids_str`;
            False otherwise
        """

        course_ids = self._ids_converter(course_ids_str)
        c_ids = {c.id for c in self.courses}

        new_courses = tuple(
            self.get_course(i) for i in course_ids if i not in c_ids)
        self.courses.extend(new_courses)

        for c in course_ids_str:
            if c not in self.timings:
                self.timings[c] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

            if c not in self.due_week:
                self.due_week[c] = []

            if c not in self.due_day:
                self.due_day[c] = []

        for c in new_courses:
            modules_file = f"{COURSES_DIRECTORY}/{c.id}/modules.txt"
            watchers_file = f"{COURSES_DIRECTORY}/{c.id}/watchers.txt"
            self.store_channels_in_file(self._live_channels, watchers_file)

            if self._live_channels:
                create_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:
                    self.download_modules(c, get_unpublished_modules)
    def delete_channels_from_file(text_channels: List[discord.TextChannel], file_path: str):
        """
        For each text channel provided, we remove its id from the file with given path
        if the id is contained in the file.
        """

        create_file.create_file_if_not_exists(file_path)

        with open(file_path, "r") as f:
            channel_ids = f.readlines()

        ids_to_remove = set(map(lambda channel: str(channel.id) + "\n", text_channels))

        with open(file_path, "w") as f:
            for channel_id in channel_ids:
                if channel_id not in ids_to_remove:
                    f.write(channel_id)
Beispiel #10
0
    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)
Beispiel #11
0
def startup():
    files = ("data/poll.json", "data/canvas.json", "data/piazza.json")

    for f in files:
        if not isfile(f):
            create_file_if_not_exists(f)
            bot.writeJSON({}, f)

    bot.poll_dict = bot.loadJSON("data/poll.json")
    bot.canvas_dict = bot.loadJSON("data/canvas.json")
    bot.piazza_dict = bot.loadJSON("data/piazza.json")

    for channel in filter(lambda ch: not bot.get_channel(int(ch)), list(bot.poll_dict)):
        del bot.poll_dict[channel]

    for channel in (c for g in bot.guilds for c in g.text_channels if str(c.id) not in bot.poll_dict):
        bot.poll_dict.update({str(channel.id): ""})

    bot.writeJSON(bot.poll_dict, "data/poll.json")

    Canvas.canvas_init(bot.get_cog("Canvas"))
    Piazza.piazza_start(bot.get_cog("Piazza"))
    def track_course(self, course_ids_str: Tuple[str, ...]):
        """
        Adds course(s) to track

        Parameters
        ----------
        course_ids_str : `Tuple[str, ...]`
            Tuple of course ids
        """

        course_ids = self._ids_converter(course_ids_str)
        c_ids = {c.id for c in self.courses}

        new_courses = tuple(self.get_course(i) for i in course_ids if i not in c_ids)
        self.courses.extend(new_courses)

        for c in course_ids_str:
            if c not in self.timings:
                self.timings[c] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

            if c not in self.due_week:
                self.due_week[c] = []

            if c not in self.due_day:
                self.due_day[c] = []

        for c in new_courses:
            modules_file = f"{COURSES_DIRECTORY}/{c.id}/modules.txt"
            watchers_file = f"{COURSES_DIRECTORY}/{c.id}/watchers.txt"
            self.store_channels_in_file(tuple(self._live_channels), watchers_file)

            if self._live_channels:
                create_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:
                    self.download_modules(c)
Beispiel #13
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.")
Beispiel #14
0
    async def check_modules(self):
        """
        For every folder in handler.canvas_handler.COURSES_DIRECTORY (abbreviated as CDIR) we will:
        - get the modules for the Canvas course with ID that matches the folder name
        - compare the modules we retrieved with the modules found in CDIR/{course_id}/modules.txt
        - send the names of any new modules (i.e. modules that are not in modules.txt) to all channels
          in CDIR/{course_id}/watchers.txt
        - update CDIR/{course_id}/modules.txt with the modules we retrieved from Canvas

        NOTE: the Canvas API distinguishes between a Module and a ModuleItem. In our documentation, though,
        the word "module" can refer to both; we do not distinguish between the two types.
        """
        def get_field_value(module: Union[Module, ModuleItem]) -> str:
            """
            This function returns a string that can be added to a Discord embed as a field's value. The returned
            string contains the module's name/title attribute (depending on which one it has), as well
            as a hyperlink to the module (if the module has the html_url attribute). If the module's name/title exceeds
            MAX_MODULE_IDENTIFIER_LENGTH characters, we truncate it and append an ellipsis (...) so that the name/title
            has MAX_MODULE_IDENTIFIER_LENGTH characters.
            """

            if hasattr(module, "title"):
                field = module.title
            else:
                field = module.name

            if len(field) > MAX_MODULE_IDENTIFIER_LENGTH:
                field = f"{field[:MAX_MODULE_IDENTIFIER_LENGTH - 3]}..."

            if hasattr(module, "html_url"):
                field = f"[{field}]({module.html_url})"

            return field

        def update_embed(embed: discord.Embed, module: Union[Module,
                                                             ModuleItem],
                         embed_list: List[discord.Embed]):
            """
            Adds a field to embed containing information about given module. The field includes the module's name or
            title, as well as a hyperlink to the module if one exists.

            If the module's identifier (its name or title) has over MAX_IDENTIFIER_LENGTH characters, we truncate the
            identifier and append an ellipsis (...) so that the length does not exceed the maximum.

            The embed object that is passed in must have at most 24 fields.

            A deep copy of the embed object is appended to embed_list in two cases:
            - if adding the new field will cause the embed to exceed EMBED_CHAR_LIMIT characters in length
            - if the embed has 25 fields after adding the new field
            In both cases, we clear all of the original embed's fields after adding the embed copy to embed_list.

            NOTE: changes to embed and embed_list will persist outside this function.
            """

            field_value = get_field_value(module)

            # Note: 11 is the length of the string "Module Item"
            if 11 + len(field_value) + len(embed) > EMBED_CHAR_LIMIT:
                embed_list.append(copy.deepcopy(embed))
                embed.clear_fields()
                embed.title = f"New modules found for {course.name} (continued):"

            if isinstance(module, Module):
                embed.add_field(name="Module", value=field_value, inline=False)
            else:
                embed.add_field(name="Module Item",
                                value=field_value,
                                inline=False)

            if len(embed.fields) == 25:
                embed_list.append(copy.deepcopy(embed))
                embed.clear_fields()
                embed.title = f"New modules found for {course.name} (continued):"

        def write_modules(file_path: str, modules: List[Union[Module,
                                                              ModuleItem]]):
            """
            Stores the ID of all modules in file with given path.
            """

            with open(file_path, "w") as f:
                for module in modules:
                    f.write(str(module.id) + "\n")

        def get_embeds(
                modules: List[Union[Module,
                                    ModuleItem]]) -> List[discord.Embed]:
            """
            Returns a list of Discord embeds to send to live channels.
            """

            embed = discord.Embed(
                title=f"New modules found for {course.name}:",
                color=CANVAS_COLOR)
            embed.set_thumbnail(url=CANVAS_THUMBNAIL_URL)

            embed_list = []

            for module in modules:
                update_embed(embed, module, embed_list)

            if len(embed.fields) != 0:
                embed_list.append(embed)

            return embed_list

        if os.path.exists(util.canvas_handler.COURSES_DIRECTORY):
            courses = [
                name
                for name in os.listdir(util.canvas_handler.COURSES_DIRECTORY)
            ]

            # each folder in the courses directory is named with a course id (which is a positive integer)
            for course_id_str in courses:
                if course_id_str.isdigit():
                    course_id = int(course_id_str)

                    try:
                        course = CANVAS_INSTANCE.get_course(course_id)
                        modules_file = f"{util.canvas_handler.COURSES_DIRECTORY}/{course_id}/modules.txt"
                        watchers_file = f"{util.canvas_handler.COURSES_DIRECTORY}/{course_id}/watchers.txt"

                        create_file_if_not_exists(modules_file)
                        create_file_if_not_exists(watchers_file)

                        with open(modules_file, "r") as m:
                            existing_modules = set(m.read().splitlines())

                        all_modules = CanvasHandler.get_all_modules(
                            course, self.bot.notify_unpublished)
                        write_modules(modules_file, all_modules)
                        differences = list(
                            filter(
                                lambda module: str(module.id) not in
                                existing_modules, all_modules))

                        embeds_to_send = get_embeds(differences)

                        if embeds_to_send:
                            with open(watchers_file, "r") as w:
                                for channel_id in w:
                                    channel = self.bot.get_channel(
                                        int(channel_id.rstrip()))
                                    notify_role = next(
                                        (r for r in channel.guild.roles
                                         if r.name.lower() == "notify"), None)
                                    await channel.send(notify_role.mention
                                                       if notify_role else "")

                                    for element in embeds_to_send:
                                        await channel.send(embed=element)
                    except Exception:
                        print(traceback.format_exc(), flush=True)