Example #1
0
    async def add_person(self, ctx: commands.Context, *args):
        """Add a faculty member to the email registry

		Usage:
		```
		++addperson @member channel-mentions
		```or
		```
		++addperson name surname channel-mentions
		```
		Arguments:
		> **@member**: A mention of the person you want to add, if they are in the server
		> **name**: The first name of the person you want to add (use quotes if it contains a space)
		> **surname**: The last name of the person you want to add
		> **channel-mentions**: A (possibly empty) space separated list of #channel-mentions which the professor teaches, or #ask-the-staff for a member of the administration
		"""
        if ctx.message.mentions:
            if len(ctx.message.mentions) > 1:
                raise FriendlyError("Please mention only one member.")
            full_name = Member(ctx.message.mentions[0]).base_name
            member_id = ctx.message.mentions[0].id
            name, surname = full_name.rsplit(" ", 1)
        else:
            name, surname = (s.strip() for s in args[0:2])
            member_id = None
            if "<" in {name[0], surname[0]}:
                raise FriendlyError(
                    "The first two arguments must be a first name and a last name if"
                    " you haven't mentioned the person you want to add.")
        person_id = self.person_adder.add_person(name, surname,
                                                 ctx.message.channel_mentions,
                                                 self.categoriser, member_id)
        person = self.finder.get_people([person_id])
        await ctx.send(embed=self.embedder.gen_embed(person))
async def next_paragraph(ctx: commands.Context, last_paragraph):
	"""finds the next paragraph in the last searched wiki"""
	channel_id = ctx.channel.id
	if last_paragraph[channel_id] != None:
		try:  # attempt to get the next paragraph in the wiki
			next_paragraph = last_paragraph[channel_id].find_next("p")
			last_paragraph[channel_id] = next_paragraph
			await ctx.send(remove_citations(next_paragraph.get_text()))

		except discord.HTTPException:  # raised when trying to get a non existent piece of html
			last_paragraph[channel_id] = None
			raise FriendlyError("The end of the search has been reached.", ctx.channel)
	else:
		raise FriendlyError("There is no search to continue.", ctx.channel)
Example #3
0
    async def calendar_links(self, ctx, *args):
        """
		Get the links to add or view the calendar

		Usage:
		```
		++calendar.links
		```
		If you have multiple class roles:
		```
		++calendar.links <Class Name>
		```
		Arguments:
		**<Class Name>**: The calendar to get links for (ex. "Lev 2023").
		"""
        try:
            calendar_name = " ".join(args) if args else None
            calendar = self.finder.get_calendar(ctx.author, calendar_name)
        except (ClassRoleError, ClassParseError) as error:
            raise FriendlyError(error.args[0], ctx.channel, ctx.author)
        # fetch links for calendar
        links = self.calendar_service.get_links(calendar.id)
        embed = self.calendar_embedder.embed_link(
            f"🔗 Calendar Links for {calendar.name}", links)
        await ctx.send(embed=embed)
Example #4
0
    async def join(self, ctx: commands.Context):
        """
		Join command to get new users information and place them in the right roles

		Usage:
		```
		++join first name, last name, campus, year
		```
		Arguments:

			> **first name**: Your first name
			> **last name**: Your last name
			> **campus**: Lev or Tal
			> **year**: an integer from 1 to 4 (inclusive)

		"""
        try:
            parser = JoinParser(ctx.message.content)
            await self.assigner.assign(ctx.author, parser.name(),
                                       parser.campus(), parser.year())
        except JoinParseError as err:
            if ctx.author not in self.attempts:
                self.attempts[ctx.author] = 0
            err_msg = str(err)
            if self.attempts[ctx.author] > 1:
                err_msg += (
                    f"\n\n{utils.get_discord_obj(ctx.guild.roles, 'ADMIN_ROLE').mention}"
                    f" Help! {ctx.author.mention} doesn't seem to be able to read"
                    " instructions.")
            self.attempts[ctx.author] += 1
            raise FriendlyError(err_msg, ctx.channel, ctx.author)
Example #5
0
 async def __link_unlink(self, args, ctx: commands.Context, sep_word: str,
                         func):
     # search for professor's detailschannel-mentions
     try:
         index_to = len(args) - 1 - args[::-1].index(sep_word)  # last index
     except ValueError as e:
         raise FriendlyError(
             f'You must include the word "{sep_word}" in between your query and the'
             " channel mentions",
             ctx.channel,
         )
     person = self.finder.search_one(args[:index_to], ctx.channel)
     success, error_msg = func(person.id, args[index_to + 1:])
     if not success:
         raise FriendlyError(error_msg, ctx.channel, ctx.author)
     person = self.finder.get_people([person.id])
     await ctx.send(embed=self.embedder.gen_embed(person))
Example #6
0
async def wait_for_reaction(
    bot: commands.Bot,
    message: discord.Message,
    emoji_list: Iterable[str],
    allowed_users: Iterable[discord.Member] = None,
    timeout: int = 60,
) -> int:
    """Add reactions to message and wait for user to react with one.
	Returns the index of the selected emoji (integer in range 0 to len(emoji_list) - 1)
	
	Arguments:
	<bot>: str - the bot user
	<message>: str - the message to apply reactions to
	<emoji_list>: Iterable[str] - list of emojis as strings to add as reactions
	[allowed_users]: Iterable[discord.Member] - if specified, only reactions from these users are accepted
	[timeout]: int - number of seconds to wait before timing out
	"""
    def validate_reaction(reaction: discord.Reaction,
                          user: discord.Member) -> bool:
        """Validates that:
			- The reaction is on the message currently being checked
			- The emoji is one of the emojis on the list
			- The reaction is not a reaction by the bot
			- The user who reacted is one of the allowed users
		"""
        return (reaction.message == message
                and str(reaction.emoji) in emoji_list and user != bot.user
                and (allowed_users is None or user in allowed_users))

    # add reactions to the message
    for emoji in emoji_list:
        await message.add_reaction(emoji)

    try:
        # wait for reaction (returns reaction and user)
        reaction, _ = await bot.wait_for("reaction_add",
                                         check=validate_reaction,
                                         timeout=timeout)
    except asyncio.TimeoutError as error:
        # clear reactions
        await message.clear_reactions()
        # raise timeout error as friendly error
        raise FriendlyError(
            f"You did not react within {timeout} seconds",
            message.channel,
            allowed_users[0] if len(allowed_users) == 1 else None,
            error,
        )
    else:
        # clear reactions
        await message.clear_reactions()
        # return the index of the emoji selection
        return emoji_list.index(str(reaction.emoji))
Example #7
0
    async def addmanager(self, ctx: commands.Context, *args):
        """
		Add a Google account as a manager of your class's calendar(s)

		Usage:
		```
		++calendar.grant <email>
		```
		If you have more than one class role:
		```
		++calendar.grant <email> <Class Name>
		```
		Arguments:
		**<email>**: Email address to add as a calendar manager.
		**<Class Name>**: The calendar to grant access to (ex. "Lev 2023"). Only necessary if you have more than one class role.
		"""
        # check if calendar was specified
        calendar_match = re.search(r"\b(\w{3} \d{4})", " ".join(args))
        # get calendar
        try:
            calendar_name = calendar_match.groups(
            )[0] if calendar_match else None
            calendar = self.finder.get_calendar(ctx.author, calendar_name)
        except (ClassRoleError, ClassParseError) as error:
            raise FriendlyError(error.args[0], ctx.channel, ctx.author)
        # validate email address
        email = args[0]
        if not is_email(email):
            raise FriendlyError("Invalid email address", ctx.channel,
                                ctx.author)
        # add manager to calendar
        if self.calendar_service.add_manager(calendar.id, email):
            embed = embed_success(
                f":office_worker: Successfully added manager to {calendar.name}."
            )
            await ctx.send(embed=embed)
        else:
            raise FriendlyError("An error occurred while applying changes.",
                                ctx.channel, ctx.author)
 def search_one(
     self,
     query: Iterable[str],
     curr_channel: discord.TextChannel,
 ) -> Person:
     """returns a single person who best match the query, or raise a FriendlyError if it couldn't find exactly one."""
     people = self.search(query, curr_channel)
     if not people:
         raise FriendlyError(
             "Unable to find someone who matches your query. Check your spelling or"
             " try a different query. If you still can't find them, You can add"
             f" them with `{config.prefix}addperson`.",
             curr_channel,
         )
     if len(people) > 1:
         raise FriendlyError(
             "I cannot accurately determine which of these people you're"
             " referring to. Please provide a more specific query.\n" +
             ", ".join(person.name for person in people),
             curr_channel,
         )
     return next(iter(people))
async def send_message(
	ctx: commands.Context, searched_string: str, wiki_string: str, link
):
	"""formats and sends the message"""
	try:
		await ctx.send(
			utils.remove_tabs(
				f"""
				Search results for: {searched_string}
				{wiki_string}
				{link[0]}
				"""
			)
		)
	except IndexError:
		raise FriendlyError("No search results found.", ctx.channel)
Example #10
0
    async def xkcd(self, ctx: commands.Context, *args):
        """Displays the latest xkcd comic, random comics, or comics for your search terms

		Usage:
		
		`++xkcd` - displays a random xkcd comic

		`++xkcd latest` - displays the latest xkcd comic

		`++xkcd [number]` - displays an xkcd comic given its id (ex. `++xkcd 327`)

		`++xkcd [search term]` - displays a comic for your search term (ex. `++xkcd sql`)
		"""

        search = " ".join(args).lower()

        try:
            # ++xkcd latest
            if search == "latest":
                # get the latest xkcd comic
                comic = self.xkcd_fetcher.get_latest()
            # ++xkcd [num]
            elif search.isdigit():
                # get response from the xkcd API with search as the id
                comic = self.xkcd_fetcher.get_comic_by_id(int(search))
            # ++xkcd [search term]
            elif len(args) > 0:
                # get relevant xkcd for search term
                comic = self.xkcd_fetcher.search_relevant(search)
            # ++xkcd
            else:
                # get a random xkcd comic
                comic = self.xkcd_fetcher.get_random()
            # embed the response
            embed = self.xkcd_embedder.gen_embed(comic)
            # reply with the embed
            await ctx.send(embed=embed)
        except ConnectionError as error:
            # request did not return a 200 response code
            raise FriendlyError(error.args[0], ctx.channel, ctx.message.author,
                                error)
    async def handle(self, error: Exception, message: discord.Message = None):
        if isinstance(error, FriendlyError):
            await self.__handle_friendly(error, message)

        elif isinstance(error, QuietWarning):
            self.__handle_quiet_warning(error)

        elif isinstance(error, discord_err.CommandInvokeError):
            await self.handle(error.original, message)

        else:
            self.logger.log_to_file(error, message)
            user_error, to_log = self.__user_error_message(error)
            if to_log:
                await self.logger.log_to_channel(error, message)
            if message is not None:
                friendly_err = FriendlyError(
                    user_error,
                    message.channel,
                    message.author,
                    error,
                )
                await self.handle(friendly_err, message)
Example #12
0
    async def get_email(self, ctx: commands.Context, *args):
        """This command returns the email address of the person you ask for.

		Usage:
		```
		++getemail query
		```
		Arguments:
		> **query**: A string with the professor's name and/or any courses they teach (or their channels) (e.g. eitan computer science)
		"""
        people = self.finder.search(args, ctx.channel)
        people = {person for person in people if person.emails}
        if not people:
            raise FriendlyError(
                "The email you are looking for aught to be here... But it isn't."
                " Perhaps the archives are incomplete.",
                ctx.channel,
            )
        else:
            title = (
                "**_YOU_ get an email!! _YOU_ get an email!!**\nEveryone gets an email!"
            )
            embed = self.embedder.gen_embed(people)
            await ctx.send(content=title, embed=embed)
Example #13
0
    async def events_list(self, ctx: commands.Context, *args):
        """
		Display upcoming events from the Google Calendar

		Usage:
		```
		++events.list
		++events.list "<query>"
		++events.list <max_results>
		++events.list "<query>" <max_results>
		++events.list "<query>" <max_results> in <Class Name>
		```
		Arguments:
		**<query>**: The query to search for within event titles. This can be a string to search for or a channel mention. (Default: shows any events)
		**<max_results>**: The maximum number of events to display. (Default: 5 results or 15 with query)
		**<Class Name>**: The calendar to get events from (ex. "Lev 2023"). Only necessary if you have more than one class role.
		"""
        try:
            last_occurence = max(loc for loc, val in enumerate(args)
                                 if val == "in") if "in" in args else len(args)
            calendar_name = " ".join(args[last_occurence + 1:])
            calendar = self.finder.get_calendar(ctx.author, calendar_name)
            args = args[:last_occurence]
        except (ClassRoleError, ClassParseError) as error:
            raise FriendlyError(error.args[0], ctx.channel, ctx.author)
        # all arguments are query if last argument is not a number
        if len(args) > 0 and not args[-1].isdigit():
            query = " ".join(args)
        # all but last argument are query if last argument is a number
        else:
            query = " ".join(args[0:-1]) if len(args) > 1 else ""
        # convert channel mentions to full names
        full_query = self.course_mentions.replace_channel_mentions(query)
        # extract max_results - last argument if it is a number, otherwise, default value
        max_results = 5  # default value if no query
        # last argument if it's a number
        if len(args) > 0 and args[-1].isdigit():
            max_results = int(args[-1])
        # check ahead 15 results by default if there is a query
        elif query:
            max_results = 15
        # loading message
        response = await ctx.send(
            embed=embed_success("🗓 Searching for events..."))
        # set initial page number and token
        page_num = None
        page_token = None
        # display events and allow showing more with reactions
        while True:
            try:
                # fetch a page of events
                events, page_token = self.calendar_service.fetch_upcoming(
                    calendar.id, max_results, full_query, page_token)
                # initialize count if there are multiple pages
                if page_num is None and page_token:
                    page_num = 1
                # create embed
                embed = self.calendar_embedder.embed_event_list(
                    title=f"📅 Upcoming Events for {calendar.name}",
                    events=events,
                    description=f'Showing results for "{full_query}"'
                    if full_query else "",
                    page_num=page_num,
                )
                # send list of events
                await response.edit(embed=embed)
                # break when no more events
                if not page_token:
                    break
                # wait for author to respond with "⏬"
                await wait_for_reaction(bot=self.bot,
                                        message=response,
                                        emoji_list=["⏬"],
                                        allowed_users=[ctx.author])
                # increment page count
                page_num += 1
            # time window exceeded
            except FriendlyError:
                break
Example #14
0
    async def delete_event(self, ctx: commands.Context, *args):
        """
		Add events to the Google Calendar

		Usage:
		```
		++events.delete <query>
		++events.delete <query> in <Class Name>
		```
		Example:
		```
		++events.delete #calculus-1 Moed Bet
		++events.delete #digital-systems HW 10 in Lev 2023
		```
		Arguments:
		**<query>**: A keyword to look for in event titles. This can be a string to search or include a channel mention.
		**<Class Name>**: The calendar to delete the event from (ex. "Lev 2023"). Only necessary if you have more than one class role.
		"""
        # replace channel mentions with course names
        query = self.course_mentions.replace_channel_mentions(" ".join(args))
        match = re.search(r"^\s*(.*?)\s*(in \w{3} \d{4})?\s*$", query)
        [query, calendar_name
         ] = match.groups() if match is not None else [None, None]

        # get calendar
        try:
            calendar = self.finder.get_calendar(ctx.author, calendar_name)
        except (ClassRoleError, ClassParseError) as error:
            raise FriendlyError(error.args[0], ctx.channel, ctx.author)
        # loading message
        response = await ctx.send(
            embed=embed_success("🗓 Searching for events..."))
        # fetch upcoming events
        events, _ = self.calendar_service.fetch_upcoming(
            calendar.id, 50, query)
        num_events = len(events)
        # no events found
        if num_events == 0:
            await response.delete()
            raise FriendlyError(f"No events were found for '{query}'.",
                                ctx.channel, ctx.author)
        # multiple events found
        elif num_events > 1:
            embed = self.calendar_embedder.embed_event_list(
                title=f"⚠ Multiple events were found.",
                events=events,
                description=(
                    'Please specify which event you would like to delete.'
                    f'\n\nShowing results for "{query}"'),
                colour=discord.Colour.gold(),
                enumeration=self.number_emoji,
            )
            await response.edit(embed=embed)
            # ask user to pick an event with emojis
            selection_index = await wait_for_reaction(
                bot=self.bot,
                message=response,
                emoji_list=self.number_emoji[:num_events],
                allowed_users=[ctx.author],
            )
            # get the event selected by the user
            event_to_delete = events[selection_index]
        # only 1 event found
        else:
            # get the event at index 0 if there's only 1
            event_to_delete = events[0]
        # delete event
        try:
            self.calendar_service.delete_event(calendar.id, event_to_delete)
        except ConnectionError as error:
            await response.delete()
            raise FriendlyError(error.args[0], ctx.channel, ctx.author, error)
        embed = self.calendar_embedder.embed_event(
            "🗑 Event deleted successfully", event_to_delete)
        await response.edit(embed=embed)
Example #15
0
    async def update_event(self, ctx: commands.Context, *, args=None):
        """
		Add events to the Google Calendar

		Usage:
		```
		++events.update <query> [parameters to set]
		```
		Examples:
		```
		++events.update "#calculus-1 moed b" title="Moed B #calculus-1" end="12:00 PM"
		```
		```
		++events.update "#calculus-1 review class" start="July 7, 3pm" end="July 7, 5pm"
		```
		```
		++events.update "#digital-systems HW 2" description="Submission box: https://moodle.jct.ac.il/mod/assign/view.php?id=420690"
		```
		```
		++events.update "#calculus-1 moed bet" title="Moed B #calculus-1" in Lev 2023
		```
		Arguments:
		**<query>**: A keyword to look for in event titles. This can be a string to search or include a channel mention.
		**[parameters to set]**: List of parameters in the form `title="new title"`. See below for the list of parameters.
		**<Class Name>**: The calendar to update the event in (ex. "in Lev 2023"). Only necessary if you have more than one class role.

		Allowed parameters (all are optional):
		**title**: The new title of the event.
		**start**: The new start date/time of the event (Israel time).
		**end**: The new end date/time of the event (Israel time).
		**location**: The new location of the event.
		**description**: The new description of the event.
		"""
        # check command syntax
        allowed_params = "|".join(
            ("title", "start", "end", "location", "description"))
        # check for correct pattern in message
        match = re.search(
            r'^\s*\S+\s[\'"]?(?P<query>[^"]*?)[\'"]?,?(?P<params>(?:\s*(?:%s)=\s*"[^"]*?",?)*)(?P<calendar> in \w{3} \d{4})?\s*$'
            % allowed_params,
            ctx.message.content,
        )
        if match is None:
            # did not fit pattern required for update command
            raise FriendlyError(
                "Could not figure out command syntax. Check the examples with"
                f" `{ctx.prefix}help {ctx.invoked_with}`",
                ctx.channel,
                ctx.author,
            )
        # extract query, params list, and calendar from the command
        [query, params, calendar_name] = match.groups()
        # replace channel mentions with course names
        query = self.course_mentions.replace_channel_mentions(query)
        # get calendar
        try:
            calendar = self.finder.get_calendar(ctx.author, calendar_name)
        except (ClassRoleError, ClassParseError) as error:
            raise FriendlyError(error.args[0], ctx.channel, ctx.author)
        # loading message
        response = await ctx.send(
            embed=embed_success("🗓 Searching for events..."))
        # get a list of upcoming events
        events, _ = self.calendar_service.fetch_upcoming(
            calendar.id, 50, query)
        num_events = len(events)
        # no events found
        if num_events == 0:
            await response.delete()
            raise FriendlyError(f"No events were found for '{query}'.",
                                ctx.channel, ctx.author)
        # multiple events found
        elif num_events > 1:
            embed = self.calendar_embedder.embed_event_list(
                title=f"⚠ Multiple events were found.",
                events=events,
                description=(
                    "Please specify which event you would like to update."
                    f'\n\nShowing results for "{query}"'),
                colour=discord.Colour.gold(),
                enumeration=self.number_emoji,
            )
            await response.edit(embed=embed)
            # ask user to pick an event with emojis
            selection_index = await wait_for_reaction(
                bot=self.bot,
                message=response,
                emoji_list=self.number_emoji[:num_events],
                allowed_users=[ctx.author],
            )
            # get the event selected by the user
            event_to_update = events[selection_index]
        # only 1 event found
        else:
            # get the event at index 0
            event_to_update = events[0]
        # Extract params into kwargs
        param_args = dict(
            re.findall(
                r'(?P<key>%s)\s*=\s*"(?P<value>[^"]*?)"' % allowed_params,
                params,
            ))
        # Replace channel mentions with full names
        for key, value in param_args.items():
            param_args[key] = self.course_mentions.replace_channel_mentions(
                value)
        try:
            event = self.calendar_service.update_event(calendar.id,
                                                       event_to_update,
                                                       **param_args)
        except ValueError as error:
            await response.delete()
            raise FriendlyError(error.args[0], ctx.channel, ctx.author, error)
        embed = self.calendar_embedder.embed_event(
            ":white_check_mark: Event updated successfully", event)
        await response.edit(embed=embed)
Example #16
0
    async def add_event(self, ctx: commands.Context, *args):
        """
		Add events to the Google Calendar

		Usage:
		```
		++events.add <Title> at <Start>
		++events.add <Title> on <Start> to <End>
		++events.add <Title> on <Start> to <End> in <Class Name>
		```
		Examples:
		```
		++events.add #compilers HW 3 on April 10 at 11:59pm
		```
		```
		++events.add Moed A #calculus-1 on February 9 from 9am-11am
		```
		```
		++events.add #digital-systems HW 3 on Apr 15 at 11pm in Lev 2023
		```
		Arguments:
		**<Title>**: The name of the event to add. (You can use channel mentions in here to get fully qualified course names.)
		**<Start>**: The start date and/or time of the event (Israel time).
		**<End>**: The end date and/or time of the event (Israel time). If not specified, the start time is used.
		**<Class Name>**: The calendar to add the event to (ex. "Lev 2023"). Only necessary if you have more than one class role.
		"""
        # replace channel mentions with course names
        message = self.course_mentions.replace_channel_mentions(" ".join(args))
        title = None
        times = None
        # separate title from rest of message
        title_times_separators = (" on ", " at ", " from ", " in ")
        for sep in title_times_separators:
            if sep in message:
                [title, times] = message.split(sep, 1)
                break
        # did not find a way to separate title from rest of message
        if times is None:
            raise FriendlyError(
                "Expected 'on', 'at', 'from', or 'in' to separate title from time.",
                ctx.channel,
                ctx.author,
            )
        # get calendar
        try:
            calendar_name = None
            if " in " in times:
                [times, calendar_name] = times.split(" in ", 1)
            # get calendar specified in arguments
            calendar = self.finder.get_calendar(ctx.author, calendar_name)
        except (ClassRoleError, ClassParseError) as error:
            raise FriendlyError(error.args[0], ctx.channel, ctx.author)
        # default values if no separator found
        start = times
        end = None
        # separate start and end times
        start_end_separators = (" to ", " until ", " for ", "-")
        for sep in start_end_separators:
            if sep in times:
                [start, end] = times.split(sep, 1)
                break
        if " from " in start:
            start = start.replace(" from ", " at ")
        try:
            event = self.calendar_service.add_event(calendar.id, title, start,
                                                    end)
        except ValueError as error:
            raise FriendlyError(error.args[0], ctx.channel, ctx.author, error)
        embed = self.calendar_embedder.embed_event(
            ":white_check_mark: Event created successfully", event)
        await ctx.send(embed=embed)