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)
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)
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)
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))
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))
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)
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)
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)
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
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)
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)
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)