From e9b4e445975fb265cdd5a3ec8fb7f1eff5f3e62c Mon Sep 17 00:00:00 2001 From: James Dearlove Date: Sun, 16 Jun 2024 08:26:38 +1000 Subject: [PATCH] Get rid of the unimplemented folder --- unimplemented/acronym.py | 54 ---- unimplemented/ascii.py | 115 ------- unimplemented/attic.py | 115 ------- unimplemented/cards.py | 54 ---- unimplemented/crates.py | 628 ------------------------------------ unimplemented/crisis.py | 20 -- unimplemented/define.py | 39 --- unimplemented/dice.py | 21 -- unimplemented/dog.py | 32 -- unimplemented/ecp.py | 25 -- unimplemented/help.py | 24 -- unimplemented/leet.py | 145 --------- unimplemented/meme.py | 196 ----------- unimplemented/parking.py | 66 ---- unimplemented/pokemash.py | 503 ----------------------------- unimplemented/radar.py | 14 - unimplemented/spider.py | 9 - unimplemented/techcrunch.py | 48 --- unimplemented/trivia.py | 376 --------------------- unimplemented/umart.py | 79 ----- unimplemented/uqfinal.py | 144 --------- unimplemented/urban.py | 62 ---- unimplemented/weather.py | 219 ------------- unimplemented/welcome.py | 93 ------ unimplemented/wiki.py | 45 --- unimplemented/wolfram.py | 251 -------------- unimplemented/yt.py | 63 ---- 27 files changed, 3440 deletions(-) delete mode 100644 unimplemented/acronym.py delete mode 100644 unimplemented/ascii.py delete mode 100644 unimplemented/attic.py delete mode 100644 unimplemented/cards.py delete mode 100644 unimplemented/crates.py delete mode 100644 unimplemented/crisis.py delete mode 100644 unimplemented/define.py delete mode 100644 unimplemented/dice.py delete mode 100644 unimplemented/dog.py delete mode 100644 unimplemented/ecp.py delete mode 100644 unimplemented/help.py delete mode 100644 unimplemented/leet.py delete mode 100644 unimplemented/meme.py delete mode 100644 unimplemented/parking.py delete mode 100644 unimplemented/pokemash.py delete mode 100644 unimplemented/radar.py delete mode 100644 unimplemented/spider.py delete mode 100644 unimplemented/techcrunch.py delete mode 100644 unimplemented/trivia.py delete mode 100644 unimplemented/umart.py delete mode 100644 unimplemented/uqfinal.py delete mode 100644 unimplemented/urban.py delete mode 100644 unimplemented/weather.py delete mode 100644 unimplemented/welcome.py delete mode 100644 unimplemented/wiki.py delete mode 100644 unimplemented/wolfram.py delete mode 100644 unimplemented/yt.py diff --git a/unimplemented/acronym.py b/unimplemented/acronym.py deleted file mode 100644 index 0383f889..00000000 --- a/unimplemented/acronym.py +++ /dev/null @@ -1,54 +0,0 @@ -from uqcsbot import bot, Command -from requests import get -from urllib.parse import quote -from bs4 import BeautifulSoup -from typing import List, Tuple -from functools import partial -import asyncio -from uqcsbot.utils.command_utils import UsageSyntaxException - -ACRONYM_LIMIT = 5 -BASE_URL = "http://acronyms.thefreedictionary.com" - - -async def get_acronyms(loop, word: str) -> Tuple[str, List[str]]: - http_response = await loop.run_in_executor(None, partial(get, f"{BASE_URL}/{quote(word)}")) - html = BeautifulSoup(http_response.content, 'html.parser') - acronym_tds = html.find_all("td", class_="acr") - return word, [td.find_next_sibling("td").text for td in acronym_tds] - - -@bot.on_command("acro") -def handle_acronym(command: Command): - """ - `!acro ` - Finds an acronym for the given text. - """ - if not command.has_arg(): - raise UsageSyntaxException() - - words = command.arg.split(" ") - - # Requested by @wbo, do not remove unless you get his express permission - if len(words) == 1: - word = words[0] - if word.lower() in [":horse:", "horse"]: - bot.post_message(command.channel_id, ">:taco:") - return - elif word.lower() in [":rachel:", "rachel"]: - bot.post_message(command.channel_id, ">:older_woman:") - return - - loop = bot.get_event_loop() - acronym_futures = [get_acronyms(loop, word) for word in words[:ACRONYM_LIMIT]] - response = "" - for word, acronyms in loop.run_until_complete(asyncio.gather(*acronym_futures)): - if acronyms: - acronym = acronyms[0] - response += f">{word.upper()}: {acronym}\r\n" - else: - response += f"{word.upper()}: No acronyms found!\r\n" - - if len(words) > ACRONYM_LIMIT: - response += f">I am limited to {ACRONYM_LIMIT} acronyms at once" - - bot.post_message(command.channel_id, response) diff --git a/unimplemented/ascii.py b/unimplemented/ascii.py deleted file mode 100644 index b294f137..00000000 --- a/unimplemented/ascii.py +++ /dev/null @@ -1,115 +0,0 @@ -from uqcsbot import bot, Command -from requests import get -from requests.exceptions import RequestException -from uqcsbot.utils.command_utils import loading_status -import random - -NO_QUERY_MESSAGE = "Can't ASCIIfy nothing... try `!asciify `" -BOTH_OPTIONS_MESSAGE = "Font can only be random OR specified" -ERROR_MESSAGE = "Trouble with HTTP Request, can't ASCIIfy :(" -NO_FONT_MESSAGE = "Cannot find the specified font in the fontslist." -ASCII_URL = "http://artii.herokuapp.com/make?text=" -FONT_URL = "http://artii.herokuapp.com/fonts_list" - - -@bot.on_command("asciify") -@loading_status -def handle_asciify(command: Command): - """ - `!asciify [--fontslist] [--randomfont | --] ` - Returns ASCIIfyed text. - `--fontslist` also returns a URL to available fonts, - `--randomfont` returns, well... a random font. - A custom font from the fonts list can also be specified. - """ - # Makes sure the query is not empty - if not command.has_arg(): - bot.post_message(command.channel_id, NO_QUERY_MESSAGE) - return - command_args = command.arg.split() - random_font = False - custom_font = False - return_fonts = False - # check for font list option - if '--fontslist' in command_args: - return_fonts = True - command_args.remove('--fontslist') - # check for random font option - if '--randomfont' in command_args: - random_font = True - command_args.remove('--randomfont') - # check for custom font option - fontslist = get_fontslist() - if not fontslist: - bot.post_message(command.channel_id, ERROR_MESSAGE) - return - for i in command_args: - if '--' in i: - if i.strip('--') in fontslist: - custom_font = True - selected_font = i.strip('--') - command_args.remove(i) - break - else: - bot.post_message(command.channel_id, NO_FONT_MESSAGE) - return - # check for invalid options - if random_font and custom_font: - bot.post_message(command.channel_id, BOTH_OPTIONS_MESSAGE) - return - if not command_args: - text = None - else: - text = ' '.join(command_args) - # asciification - if text is None: - bot.post_message(command.channel_id, NO_QUERY_MESSAGE) - ascii_text = None - else: - if random_font: - font = get_random_font() - elif custom_font: - font = selected_font - else: - font = None - ascii_text = asciify(text, font) - if ascii_text is None: - bot.post_message(command.channel_id, ERROR_MESSAGE) - return - # message posts - if return_fonts: - bot.post_message(command.channel_id, FONT_URL) - if ascii_text: - bot.post_message(command.channel_id, ascii_text) - else: - return - return - - -def asciify(text: str, font: str) -> str: - try: - if font is not None: - url = ASCII_URL + text + '&font=' + font - else: - url = ASCII_URL + text - resp = get(url) - ascii_text = f"```\n{resp.text}\n```" - return ascii_text - except RequestException: - return None - - -def get_random_font() -> str: - fontslist = get_fontslist() - if fontslist: - return random.choice(tuple(fontslist)) - else: - return None - - -def get_fontslist() -> set: - try: - resp = get('http://artii.herokuapp.com/fonts_list') - fontslist = set(resp.text.split()) - return fontslist - except RequestException: - return None diff --git a/unimplemented/attic.py b/unimplemented/attic.py deleted file mode 100644 index 1ec705e4..00000000 --- a/unimplemented/attic.py +++ /dev/null @@ -1,115 +0,0 @@ -from uqcsbot import bot, Command -from uqcsbot.utils.command_utils import loading_status -from typing import List -import os -import requests - - -BASE_FOLDER_URL = 'https://drive.google.com/drive/folders/' -BASE_FILE_URL = 'https://drive.google.com/file/d/' -BASE_API_URL = 'https://www.googleapis.com/drive/v3/' -BASE_ATTIC_FOLDER = '0B6_D4T6LJ-uwZmFhMzIyNGYtNTM2OS00ZDJlLTg0NmYtY2IyNzA1MDZlNDIx' -API_KEY = os.environ.get('GOOGLE_API_KEY') -ROOM_FILE_LIMIT = 15 # Number of files allowed to be posted in room, else sent via direct message. - - -def format_files(files: List[dict]) -> List[str]: - """ - Takes the list of file dictionaries found and formats - them into Slack-appropriate message strings. - - :param files: a list of dictionaries where each dictionary - represents a file in the Google Drive folder. - :param course: a dictionary representing the parent Google Drive folder for the queried course. - :return: a list of strings to be sent as messages. - """ - sorted_files = sorted(files, key=lambda f: f['name']) # Sort alphabetically by filename. - return [f'>*{file["name"]}:* <{BASE_FILE_URL}{file["id"]}|Link>' for file in sorted_files] - - -def get_all_files(folder: dict) -> List[dict]: - """ - Takes a parent folder and recursively produces a single-level list - of all files contained in that folder and any subdirectories. - - :param folder: a dictionary representing a Google Drive folder. - :return: a single-dimensional list of file dictionaries. - """ - files = [] - for file in get_folder_contents(folder): - if file['mimeType'] == 'application/vnd.google-apps.folder': - files.extend(get_all_files(file)) - else: - files.append(file) - return files - - -def get_folder_contents(folder: dict) -> List[dict]: - """ - Gets all files and folders inside the given Google Drive folder dictionary. - Note: does not return the contents of subdirectories. - - :param folder: a dictionary representing a Google Drive folder to get the contents of. - :return: a list of Google drive folder/file dictionaries - representing all contents of the folder. - """ - folder_url = f"{BASE_API_URL}files?q='{folder['id']}' in parents&key={API_KEY}" - http_response = requests.get(folder_url) - if http_response.status_code == 200: - return http_response.json()['files'] - else: - # I figure it's better to ignore a failed folder fetch than provide no response/error out. - return [] - - -@bot.on_command('attic') -@loading_status -def handle_attic(command: Command) -> None: - """ - `!attic [COURSE CODE]` - Returns a list of links to all documents in the course folder for UQ - Attic, the unofficial exam solution and study material repository. Defaults to searching for - the name of the current channel unless explicitly provided a course code (e.g. CSSE1001). - """ - channel = bot.channels.get(command.channel_id) - course_code = command.arg if command.arg is not None else channel.name - course_code = course_code.upper() - - # Make request for UQAttic root directory contents. - root_directory_request_url = (f"{BASE_API_URL}files?q='{BASE_ATTIC_FOLDER}'" - + " in parents and mimeType = 'application/vnd.google-apps" - + f".folder'&pageSize=1000&key={API_KEY}") - root_directory = requests.get(root_directory_request_url) - if not root_directory.status_code == 200: - bot.post_message(channel, 'There was an error getting the root UQAttic directory.') - return - root_directory_data = root_directory.json() - - # Check course folder exists by checking for the course code in the 'name' of each file/folder. - course = next(( - item - for item in root_directory_data['files'] - if item['name'] == course_code - ), None) - if course is None: - bot.post_message(channel, f'No course folder found for {course_code}.') - return - - # Get all files in directory and subdirectories. - files = get_all_files(course) - - # Determine whether to send to user or channel (based on number of responses). - if len(files) > ROOM_FILE_LIMIT: - bot.post_message(channel, 'Too many files to list here, sent the list directly to ' - f'<@{command.user_id}>.') - response_channel = command.user_id - else: - response_channel = channel - - # Send response message with formatted list of files. - if len(files) > 0: - response_message = ('All of the UQAttic files found for the course' - + f' <{BASE_FOLDER_URL}{course["id"]} | {course["name"]}>' - + ' are listed below:\n' + '\n'.join(format_files(files))) - else: - response_message = f'There were no files found in the {course_code} course folder.' - bot.post_message(response_channel, response_message) diff --git a/unimplemented/cards.py b/unimplemented/cards.py deleted file mode 100644 index 353aba7e..00000000 --- a/unimplemented/cards.py +++ /dev/null @@ -1,54 +0,0 @@ -from random import shuffle -from uqcsbot import bot, Command - - -def emojify(value: int): - if value == -1: - return ":card-joker:" - suit = ["hearts", "spades", "diamonds", "clubs"][value//13] - rank = ["ace", "king", "queen", "jack", "10", "9", "8", "7", - "6", "5", "4", "3", "2"][value % 13] - return ":card-{}-{}:".format(rank, suit) - - -@bot.on_command("cards") -def handle_cards(command: Command): - """ - `!cards [number] [joker]` - Deals one or more cards - """ - - # easter egg - prepare four 500 hands, and the kitty - if command.arg == "500": - deck = (list(range(0, 0+10)) + list(range(13, 13+11)) + - list(range(26, 26+10)) + list(range(39, 39+11)) + [-1]) - shuffle(deck) - hands = [deck[0:10], deck[10:20], deck[20:30], deck[30:40], deck[40:]] - for i in range(5): - h = [emojify(j) for j in sorted(hands[i])] - response = [":regional-indicator-n: ", ":regional-indicator-e: ", - ":regional-indicator-s: ", ":regional-indicator-w: ", - ":cat: "][i] + "".join(h) - bot.post_message(command.channel_id, response) - return - - deck = list(range(52)) - - # add joker - if command.has_arg() and command.arg.split(" ")[-1][0].lower() == "j": - deck.append(-1) - - # set number to deal - if command.has_arg() and command.arg.split(" ")[0].isnumeric(): - cards = min(max(int(command.arg.split(" ")[0]), 1), len(deck)) - else: - cards = 1 - - shuffle(deck) - deck = deck[:cards] - deck.sort() - - response = "" - for i in deck: - response += emojify(i) - - bot.post_message(command.channel_id, response) diff --git a/unimplemented/crates.py b/unimplemented/crates.py deleted file mode 100644 index 8a5c2f7c..00000000 --- a/unimplemented/crates.py +++ /dev/null @@ -1,628 +0,0 @@ -import argparse -import json -from abc import ABC, abstractmethod -from enum import Enum -from typing import NamedTuple, Union, Optional, List, Dict, Tuple - -import requests - -from uqcsbot import bot, Command -from uqcsbot.api import Channel -from uqcsbot.utils.command_utils import loading_status, UsageSyntaxException - -BASE_URL = "https://crates.io/api/v1" -MAX_LIMIT = 15 # The maximum number of search results from one call to the command - -# NamedTuple for the case that the argparse finds a -h flag -HelpCommand = NamedTuple('HelpCommand', [('help_string', str)]) - -# NamedTuple for the default command that finds a crate by exact name match -ExactCrate = NamedTuple('ExactCrate', [('name', str)]) - -# NamedTuple for the search sub-command that deals with searching for specific crates -CrateSearch = NamedTuple('CrateSearch', [('name', str), ('limit', int), ('category', str), - ('user', str), ('sort', str), ('search', str)]) - -# NamedTuple for the default categories sub-command that deals with searching for categories -CategorySearch = NamedTuple('CategorySearch', [('name', str), ('sort', str)]) - -# NamedTuple for the user sub-command that deals with searching for specific users -UserSearch = NamedTuple('UserSearch', [('username', str)]) - -# Named tuple for a crate that was found in a search -CrateResult = NamedTuple('CrateResult', [('name', str), ('downloads', int), - ('homepage', str), ('description', str)]) - -# Named tuple for a category that was found in a search -CategoryResult = NamedTuple('CategoryResult', [('name', str), ('description', str), - ('crates', int)]) - -# Named tuple for a user that was found in a search -UserResult = NamedTuple('UserResult', [('id', int), ('username', str), ('name', str), - ('avatar', str), ('url', str)]) - - -class SlackBlock(ABC): - """ - Abstract class for a formatted slack block - (follows the conventions from https://api.slack.com/reference/messaging/blocks) - """ - - @abstractmethod - def get_formatted_block(self) -> Dict[str, str]: - """ - Gets the dictionary which maps the required properties for the given block - """ - pass - - -class ImageBlock(SlackBlock): - """ - SlackBlock that represents an image block - (contains an image url and alternate text for that image) - """ - - def __init__(self, url: str, alt_text: str): - self.url = url - self.alt_text = alt_text - - def get_formatted_block(self): - return {'type': 'image', 'image_url': self.url, 'alt_text': self.alt_text} - - -class TextBlock(SlackBlock): - """ - SlackBlock that represents a text block - (contains an image url and alternate text for that image) - """ - - def __init__(self, text: str, markdown: bool = True): - self.text = text - self.markdown = markdown - - def get_formatted_block(self): - return {'type': 'mrkdwn' if self.markdown else 'plain_text', - 'text': self.text} - - -class SubCommand(Enum): - """ - Distinguishes the type of sub command that was invoked - """ - EXACT = 1, - SEARCH = 2, - CATEGORIES = 3, - USERS = 4 - - -@bot.on_command('crates') -@loading_status -def handle_crates(command: Command): - """ - `!crates [-h] [[name] | {search,categories,users}]` - - Get information about crates from crates.io - """ - args = parse_arguments(command.arg if command.has_arg() else '') - - # Executes the function that was stored by the arg - # parser depending on which sub-command was used - args.execute_action(command.channel_id, args) # type: ignore - - -def parse_arguments(arg_str: str) -> argparse.Namespace: - """ - Parses the arguments passed to the command - :param arg_str: The argument string (not including "!crates") - """ - parser = argparse.ArgumentParser(prog="!crates", add_help=False) - subparsers = parser.add_subparsers() - - # Change the parsers default on error behaviour - def usage_error(*args, **kwargs): - raise UsageSyntaxException() - - parser.error = usage_error # type: ignore - - # Converts "Date and time" into "date-and-time" which is the format used for category ids - def category_formatter(cat: str): - return cat.lower().strip().replace(' ', '-') - - def search_limit(val: str): - return max(1, min(int(val), MAX_LIMIT)) # limits the val such that 0 < val <= MaxLimit - - # For "!crates {args}" - main_parser = subparsers.add_parser('main', add_help=False) - main_parser.add_argument('name', nargs='?', default='', type=str.lower, - help="The name of the crate to get information about") - main_parser.add_argument('-h', '--help', action='store_true', help='Prints this help message') - main_parser.set_defaults(execute_action=handle_exact_crate_route, route=SubCommand.EXACT) - - # For !crates search {args} - search_parser = subparsers.add_parser('search', add_help=False, - help='Sub-command to search for a crate') - search_parser.add_argument('search', nargs='?', default='', type=str.lower, - help='The string to use for the search') - search_parser.add_argument('-h', '--help', action='store_true', help='Prints this help message') - search_parser.add_argument('-l', '--limit', default=5, type=search_limit, - help='When not searching for a specific crate how' - + ' many results should be shown?' - + ' (max: ' + str(MAX_LIMIT) + ', default: %(default)s)') - search_parser.add_argument('-c', '--category', default='', type=category_formatter, - help="Limit results to crates in this category") - search_parser.add_argument('-u', '--user', default='', type=str, - help='Limit results by crate author') - search_parser.add_argument('-o', '--sort', choices=['alpha', 'downloads'], - default='downloads', type=str.lower, - help='Sort the results by alphabetical order or by number' - + ' of downloads (default: %(default)s)') - search_parser.set_defaults(execute_action=handle_search_crates_route, route=SubCommand.SEARCH) - - # For "!crates categories {args}" - category_parser = subparsers.add_parser('categories', add_help=False, - help='Sub-command to get information about' - + ' categories instead of crates') - category_parser.add_argument('-h', '--help', action='store_true', - help='Prints this help message') - category_parser.add_argument('name', nargs='?', default='', type=category_formatter, - help='Optional. Specify a specific category to get more' - + ' information about it') - category_parser.add_argument('-s', '--sort', choices=['alpha', 'crates'], - default='alpha', type=str.lower, - help='Sort the result by alphabetical order or' - + ' by number of crates in the category') - category_parser.set_defaults(execute_action=handle_categories_route, - route=SubCommand.CATEGORIES) - - # For "!crates users {args}" - users_parser = subparsers.add_parser('user', add_help=False, - help='Sub-command to get information about a username') - users_parser.add_argument('username', help='The users username') - users_parser.add_argument('-h', '--help', action='store_true', help='Prints this help message') - users_parser.set_defaults(execute_action=handle_users_route, route=SubCommand.USERS) - - # We need to check if the first argument is "categories" or "search" - # otherwise we add "main" to get around an issue were argparse will - # complain that the name isn't one of the subparser names - split_args = arg_str.split() - if not split_args or (split_args[0] != "categories" and split_args[0] != "search" - and split_args[0] != 'user'): - split_args.insert(0, "main") - - args = parser.parse_args(split_args) - - # If the arguments show that help was requested then - # change the execute_action and add the correct help string - if args.help: - args.execute_action = handle_help_route - if args.route == SubCommand.EXACT: - # Because we had to break parser up into main this - # help message needs to be manually typed to be useful - args.help_string = """ -*Usage: !crates [[name] | {search,categories,users}]* - -*Sub-Commands*: - {search,categories,users} - search Search for a crate with conditions - categories Get information about categories instead of crates - users Get information about a user from their username - -*Default Usage*: - usage: !crates [-h] [name] - - *Positional Arguments*: - name The exact name of the crate to get information about (use search for non-exact name) - - *Optional Arguments*: - -h, --help Prints this help message - """ - elif args.route == SubCommand.SEARCH: - args.help_string = search_parser.format_help() - elif args.route == SubCommand.CATEGORIES: - args.help_string = category_parser.format_help() - else: - args.help_string = users_parser.format_help() - - return args - - -def handle_help_route(channel: Channel, args: HelpCommand): - """ - This is called whenever the -h argument is invoked regardless of sub-command. - """ - bot.post_message(channel, args.help_string) - - -def get_user_id(username: str) -> int: - """ - Tries to get the users numerical id from their username. (Ex: BurntSushi -> 189). -1 on failure. - """ - url = f'{BASE_URL}/users/{username}' - response = requests.get(url) - - # If there was a problem getting a response return -1 - if response.status_code != requests.codes.ok: - return -1 - - user_data = json.loads(response.content) - - # If an error occurred then return with -1 - if 'errors' in user_data: - return -1 - - # Try to grab their id and return -1 if something goes wrong - # (which it shouldn't at this point but the API is badly documented so I added this for safety) - return int(user_data.get('user', {}).get('id', -1)) - - -def convert_crate_result(crate: Dict[str, Union[str, int]]) -> Optional[CrateResult]: - """ - Tries to convert a dictionary response from the api into a CrateResult. - Returns None on error. - """ - try: - # Sometimes the homepage is null so we try to grab - # something else if possible otherwise default to crates.io - homepage = crate['homepage'] - homepage = crate['repository'] if homepage is None else homepage - homepage = crate['documentation'] if homepage is None else homepage - homepage = "https://crates.io" if homepage is None else homepage - - return CrateResult(crate['name'], crate['downloads'], # type: ignore - homepage, crate['description']) # type: ignore - except KeyError: - return None - - -def get_crate_name_result(channel: Channel, name: str) -> Optional[CrateResult]: - """ - Get the result of searching for a specific crate by name - :param channel: The channel to post any error messages in - :param name: The name of the crate to search for - :return: The api response as a dictionary or None on error - """ - url = f'{BASE_URL}/crates/{name}' - - response = requests.get(url) - - # If there was a problem getting a response post a message to let the user know - if response.status_code != requests.codes.ok: - bot.post_message(channel, 'There was a problem getting a response.') - return None - - raw_crate_result = json.loads(response.content).get('crate', None) - - if raw_crate_result is None: - bot.post_message(channel, "There was an issue getting the crate information") - return None - - if 'errors' in raw_crate_result: - bot.post_message(channel, f"The requested crate {name} could not be found") - return None - - # Convert the raw crate result to a CrateResult - crate = convert_crate_result(raw_crate_result) - if crate is None: - bot.post_message(channel, "There was a problem getting information about the crate") - return None - - return crate - - -def create_slack_section_block(text: TextBlock, accessory: Optional[SlackBlock] = None) -> dict: - """ - Creates a "section block" as described in the slack documentation here: - https://api.slack.com/reference/messaging/blocks#section - """ - section_block = { - 'type': 'section', - 'text': text.get_formatted_block() - } - - if accessory is not None: - section_block['accessory'] = accessory.get_formatted_block() - - return section_block - - -def create_slack_context_block(elements: List[SlackBlock]) -> dict: - """ - Creates a "context block" as described in the slack documentation here: - https://api.slack.com/reference/messaging/blocks#context - """ - return { - 'type': 'context', - 'elements': [element.get_formatted_block() for element in elements], - } - - -def create_slack_divider_block() -> Dict[str, str]: - """ - Returns a "divider block" as described in the slack documentation here: - https://api.slack.com/reference/messaging/blocks#divider - """ - return { - 'type': 'divider' - } - - -def get_crate_blocks(crate: CrateResult) -> List[dict]: - """ - Converts a crate into its block based message format for posting to slack - """ - return [ - create_slack_section_block(TextBlock(f'*<{crate.homepage}|{crate.name}>*\n' - f'{crate.description}')), - create_slack_context_block([TextBlock(f'Downloads: {crate.downloads}', markdown=False)]), - create_slack_divider_block() - ] - - -def handle_exact_crate_route(channel: Channel, args: ExactCrate): - """ - Handles what happens when a single crate is being searched for by exact name - """ - crate = get_crate_name_result(channel, args.name) - if crate is None: - return - - bot.post_message(channel, '', blocks=get_crate_blocks(crate)) - - -def get_crates_search_results(channel: Channel, - search: str, - params: dict, - page: int = 1, ) -> Optional[Tuple[List[CrateResult], int]]: - """ - Gets a list of crates and the total number of results - from the api based on input parameters and the page number - :param channel: The channel to post any error messages to - :param search: The string to search for - :param params: The parameters dictionary that gets passed to requests.get - :param page: The page of the results to get from - :return: (list of crates, total number of search results) or None if an error occurred - """ - params['page'] = page - - if search: - params['letter'] = search - - url = BASE_URL + '/crates' - response = requests.get(url, params) - - # If there was a problem getting a response post a message to let the user know - if response.status_code != requests.codes.ok: - bot.post_message(channel, 'There was a problem getting a response.') - return None - - crates_results = json.loads(response.content) - raw_crates = crates_results.get('crates', []) - total = crates_results.get('meta', {}).get('total', 0) - - # Convert all of the crates to CrateResult - crates = [] - for raw_crate in raw_crates: - crate = convert_crate_result(raw_crate) - if crate is None: - bot.post_message(channel, "There was a problem getting information about a crate") - return None - - crates.append(crate) - - return crates, total - - -def handle_search_crates_route(channel: Channel, args: CrateSearch): - """ - Handles what happens when a crates are being searched for through multiple criteria - """ - # Generate the parameters to search with - params = {'sort': args.sort} - - if args.category: - params['category'] = args.category - - # If the user parameter is already a number use it as an id otherwise try to get the id - if args.user.isdigit(): - params['user_id'] = args.user - elif args.user: - user_id = get_user_id(args.user) - if user_id == -1: - bot.post_message(channel, f'The username {args.user} could not be resolved') - return - - params['user_id'] = str(user_id) - - search_result = get_crates_search_results(channel, args.search, params) - if search_result is None: - return - - crates, total = search_result - - # No crates at all were found - if not crates: - bot.post_message(channel, "No crates were found") - return - - # The beginning of the formatted response - blocks = [ - create_slack_section_block(TextBlock(f'*Showing {min(args.limit, total)}' - + f' of {total} results*')), - create_slack_divider_block() - ] - - # Iterate over all of the crates or until limit is reached. Whichever comes first. - page = 1 - remaining = args.limit - while remaining > 0: - amt = range(0, min(len(crates), remaining)) - for index in amt: - crate = crates[index] - blocks.extend(get_crate_blocks(crate)) - remaining -= 1 - - page += 1 - search_result = get_crates_search_results(channel, args.search, params, page) - if search_result is None or not search_result[0]: - break - - crates, _ = search_result - - bot.post_message(channel, '', blocks=blocks) - - -def get_category_page(channel: Channel, sort: str, page: int) -> Tuple[Optional[List[str]], int]: - """ - Returns all the names of all categories from a page of the response - :param channel: The channel to post any errors to - :param sort: The order to sort by. One of "crates" or "alpha" - :param page: The page number to get the categories from - :return: A tuple containing a list of category names (or None on error) - and the total number of categories - """ - # Get the categories - url = BASE_URL + '/categories' - response = requests.get(url, {'sort': sort, 'page': page}) # type: ignore - - if response.status_code != requests.codes.ok: - bot.post_message(channel, 'There was a problem getting the list of categories') - return None, 0 - - # Convert the json response - response_data = json.loads(response.content) - - raw_categories = response_data.get('categories') - total = response_data.get('meta', {}).get('total', 0) - - # Get the category names - categories = [cat.get('name') if 'name' in cat else cat.get('id', '') for cat in raw_categories] - - return categories, total - - -def display_all_categories(channel: Channel, args: CategorySearch): - """ - Displays just the names of all the categories in one big list - """ - categories, total = get_category_page(channel, args.sort, 1) - if categories is None: - return # Error occurred - - # Get all of the categories by incrementing page number - page = 2 - while len(categories) < total: - next_cats, _ = get_category_page(channel, args.sort, page) - if next_cats is None or not next_cats: - break - - categories.extend(next_cats) - page += 1 - - # Begin formatting the message - category_string = '\n'.join(categories) - blocks = [ - create_slack_section_block(TextBlock(f'*Displaying {total} categories:*')), - create_slack_section_block(TextBlock(f'```{category_string}```')), - ] - - bot.post_message(channel, '', blocks=blocks) - - -def display_specific_category(channel: Channel, args: CategorySearch): - """ - Displays a single category in more detail - For example !crates categories algorithms would return a description of the algorithms - category and the number of crates that falls into the algorithms category. A search for - a crate with "!crates search" can be filtered based on these categories using the -c flag. - """ - # Get the categories - url = BASE_URL + f'/categories/{args.name}' - response = requests.get(url) - - if response.status_code != requests.codes.ok: - bot.post_message(channel, f'There was a problem getting the category "{args.name}"') - return - - # Convert the json response - response_data = json.loads(response.content) - if 'errors' in response_data: - bot.post_message(channel, f'The category "{args.name}" does not exist') - return - - raw_category = response_data.get('category') - - name = raw_category.get('name') - name = raw_category.get('id') if name is None else name - desc = raw_category.get('description', 'No description provided') - - category = CategoryResult(name, desc, raw_category['crates_cnt']) - - # Format the message - blocks = [ - create_slack_section_block(TextBlock(f'*{category.name}:*')), - create_slack_section_block(TextBlock(category.description)), - create_slack_context_block([TextBlock(f'Crate Count: {category.crates}', markdown=False)]) - ] - - bot.post_message(channel, '', blocks=blocks) - - -def handle_categories_route(channel: Channel, args: CategorySearch): - """ - Handles the categories sub-command by determining whether - or not to display all categories or just on - """ - if args.name: - display_specific_category(channel, args) - else: - display_all_categories(channel, args) - - -def get_user(channel: Channel, username: str) -> Optional[UserResult]: - """ - Gets a UserResult by querying the crates.io api for the given username. - None on error. - """ - url = f'{BASE_URL}/users/{username}' - response = requests.get(url) - - if response.status_code != requests.codes.ok: - bot.post_message(channel, 'There was a problem getting the user') - return None - - raw_user = json.loads(response.content).get('user') - - if raw_user is None or 'errors' in raw_user: - bot.post_message(channel, f'User "{username}" not found') - return None - - user_id = raw_user.get('id', -1) - login = raw_user.get('login', username) - name = raw_user.get('name', username) - avatar = raw_user.get('avatar', 'https://imgur.com/gwtcGmr') # Blank avatar as a default - url = raw_user.get('url', '') - - return UserResult(user_id, login, name, avatar, url) - - -def handle_users_route(channel: Channel, args: UserSearch): - """ - Displays information about a user from their username - """ - user = get_user(channel, args.username) - - # Error occurred - if user is None: - return - - # Begin formatting the message - text = f'*{user.username}:*\n\t*ID*: {user.id}\n\t*Name:* {user.name}\n\t' - if user.url: - text += f'*Homepage:* {user.url}' - - blocks = [ - create_slack_section_block(TextBlock(text), - accessory=ImageBlock(user.avatar, 'User Avatar')), - create_slack_divider_block() - ] - - bot.post_message(channel, '', blocks=blocks) diff --git a/unimplemented/crisis.py b/unimplemented/crisis.py deleted file mode 100644 index 52ef0398..00000000 --- a/unimplemented/crisis.py +++ /dev/null @@ -1,20 +0,0 @@ -from uqcsbot import bot, Command - -RESPONSE = ("*Mental health/crisis resources*\n" - "24/7 UQ Counselling and Crisis Line: 1300 851 998\n" - "Campus Security (emergency): 07 3365 3333\n" - "Campus Security (non-emergency): 07 3365 1234\n" - "Counselling Services: https://www.uq.edu.au/student-services/counselling-services\n" - "UQ Psychology Clinic: https://clinic.psychology.uq.edu.au/therapies-and-services\n" - "UQ resources: https://about.uq.edu.au/campaigns-and-initiatives/mental-health") - - -@bot.on_command("crisis") -@bot.on_command("mentalhealth") -@bot.on_command("emergency") -def handle_crisis(command: Command): - """ - `!crisis`, `!mentalhealth` or `!emergency` - Get a list of emergency resources. - """ - - bot.post_message(command.channel_id, RESPONSE) diff --git a/unimplemented/define.py b/unimplemented/define.py deleted file mode 100644 index 83c4e911..00000000 --- a/unimplemented/define.py +++ /dev/null @@ -1,39 +0,0 @@ -from uqcsbot import bot, Command -import requests -import json -from uqcsbot.utils.command_utils import UsageSyntaxException - -API_URL = "http://api.pearson.com/v2/dictionaries/laad3/entries?limit=1" - - -@bot.on_command("define") -def define(command: Command): - """ - `!define ` - Gets the dictionary definition of TEXT - """ - query = command.arg - # Fun Fact: Empty searches return the definition of adagio - # (a piece of music to be played or sung slowly) - if not command.has_arg(): - raise UsageSyntaxException() - - http_response = requests.get(API_URL, params={'headword': query}) - - # Check if the response is OK - if http_response.status_code != requests.codes.ok: - bot.post_message(command.channel_id, "Problem fetching definition") - return - - json_data = json.loads(http_response.content) - results = json_data.get('results', []) - if len(results) == 0: - message = "No Results" - else: - # This gets the first definition of the first result. - senses = results[0].get('senses', [{}])[0] - # Sometimes there are "subsenses" for whatever reason and sometimes there aren't. - # No explanation provided. - # This gets the first subsense if there are, otherwise, just uses senses. - message = senses.get('subsenses', [senses])[0].get('definition', "Definition not available") - - bot.post_message(command.channel_id, f">>>{message}") diff --git a/unimplemented/dice.py b/unimplemented/dice.py deleted file mode 100644 index a53a41ee..00000000 --- a/unimplemented/dice.py +++ /dev/null @@ -1,21 +0,0 @@ -from random import choice -from uqcsbot import bot, Command - - -@bot.on_command("dice") -def handle_dice(command: Command): - """ - `!dice [number]` - Rolls 1 or more six sided dice (d6). - """ - if command.has_arg() and command.arg.isnumeric(): - rolls = min(max(int(command.arg), 1), 360) - else: - rolls = 1 - - response = [] - emoji = (':dice-one:', ':dice-two:', ':dice-three:', - ':dice-four:', ':dice-five:', ':dice-six:') - for i in range(rolls): - response.append(choice(emoji)) - - bot.post_message(command.channel_id, "".join(response)) diff --git a/unimplemented/dog.py b/unimplemented/dog.py deleted file mode 100644 index 0371c12f..00000000 --- a/unimplemented/dog.py +++ /dev/null @@ -1,32 +0,0 @@ -from uqcsbot import bot, Command - - -@bot.on_command("dog") -def handle_dog(command: Command): - """ - `!dog` - Like !cat, but for dog people. - """ - dog = "\n".join(("```", - " _ ", - " ,:'/ _..._ ", - " // ( `""-.._.'", - " \\| / O\\___", - " | O 4", - " | /", - " \\_ .--' ", - " (_'---'`) ", - " / `'---`() ", - " ,' | ", - " , .'` | ", - " )\\ _.-' ; ", - " / | .'` _ / ", - " /` / .' '. , | ", - "/ / / \\ ; | | ", - "| \\ | | .| | | ", - " \\ `\"| /.-' | | | ", - " '-..-\\ _.;.._ | |.;-. ", - " \\ <`.._ )) | .;-. )) ", - " (__. ` ))-' \\_ ))' ", - " `'--\"` jgs `\"\"\"` ```")) - - bot.post_message(command.channel_id, dog) diff --git a/unimplemented/ecp.py b/unimplemented/ecp.py deleted file mode 100644 index 340abc75..00000000 --- a/unimplemented/ecp.py +++ /dev/null @@ -1,25 +0,0 @@ -from uqcsbot import bot, Command -from uqcsbot.utils.command_utils import loading_status -from uqcsbot.utils.uq_course_utils import (get_course_profile_url, HttpException, - CourseNotFoundException, ProfileNotFoundException) - - -@bot.on_command('ecp') -@loading_status -def handle_ecp(command: Command): - """ - `!ecp [COURSE CODE]` - Returns the link to the latest ECP for the given course code. - If unspecified, will attempt to find the ECP for the channel the command was called from. - """ - channel = bot.channels.get(command.channel_id) - course_name = channel.name.upper() if not command.has_arg() else command.arg - try: - profile_url = get_course_profile_url(course_name) - except HttpException as e: - bot.logger.error(e.message) - bot.post_message(channel, f'An error occurred, please try again.') - return - except (CourseNotFoundException, ProfileNotFoundException) as e: - bot.post_message(channel, e.message) - return - bot.post_message(channel, f'*{course_name}*: <{profile_url}|ECP>') diff --git a/unimplemented/help.py b/unimplemented/help.py deleted file mode 100644 index 8861a1a5..00000000 --- a/unimplemented/help.py +++ /dev/null @@ -1,24 +0,0 @@ -from uqcsbot import bot, Command -from uqcsbot.utils.command_utils import success_status, get_helper_docs - - -@bot.on_command('help') -@success_status -def handle_help(command: Command): - """ - `!help [COMMAND]` - Display the helper docstring for the given command. - If unspecified, will return the helper docstrings for all commands. - """ - - # get helper docs - helper_docs = get_helper_docs(command.arg) - if len(helper_docs) == 0: - message = 'Could not find any helper docstrings.' - else: - message = '>>>' + '\n'.join(helper_docs) - - # post helper docs - if command.arg: - command.reply_with(bot, message) - else: - bot.post_message(command.user_id, message, as_user=True) diff --git a/unimplemented/leet.py b/unimplemented/leet.py deleted file mode 100644 index 4715007f..00000000 --- a/unimplemented/leet.py +++ /dev/null @@ -1,145 +0,0 @@ -from uqcsbot import bot, Command -from http import HTTPStatus -from uqcsbot.utils.command_utils import loading_status -import json -import requests -import random -from slackblocks import Attachment, SectionBlock -from typing import List, Tuple, Dict - -LC_DIFFICULTY_MAP = ["easy", "medium", "hard"] # leetcode difficulty is 1,2,3, need to map -HR_DS_API_LINK = ("https://www.hackerrank.com/rest/contests/master/tracks/" + - "data-structures/challenges?limit=200") -HR_ALG_API_LINK = ("https://www.hackerrank.com/rest/contests/master/tracks/" + - "algorithms/challenges?limit=200") -LC_API_LINK = 'https://leetcode.com/api/problems/all/' - - -COLORS = {"easy": "#5db85b", - "medium": "#f1ad4e", - "hard": "#d9534f"} - - -@bot.on_command('leet') -@loading_status -def handle_leet(command: Command) -> None: - """ - `!leet [`easy` | `medium` | `hard`] - Retrieves a set of questions from online coding - websites, and posts in channel with a random question from this set. If a difficulty - is provided as an argument, the random question will be restricted to this level of - challenge. Else, a random difficulty is generated to choose. - """ - was_random = True # Used for output later - - if command.has_arg(): - if (command.arg not in {"easy", "medium", "hard"}): - bot.post_message(command.channel_id, "Usage: !leet [`easy` | `medium` | `hard`]") - return - else: - difficulty = command.arg.lower() - was_random = False - else: - difficulty = random.choice(LC_DIFFICULTY_MAP) # No difficulty specified, randomly generate - - # List to store questions collected - questions: List[Tuple[str, str]] = [] - - # Go fetch questions from APIs - collect_questions(questions, difficulty) - selected_question = select_question(questions) # Get a random question - - # If we didn't find any questions for this difficulty, try again, probably timeout on all 3 - if (selected_question is None): - bot.post_message(command.channel_id, - "Hmm, the internet pipes are blocked. Try that one again.") - return - - # Leetcode difficulty colors - color = COLORS[difficulty] - - if (was_random): - title_text = f"Random {difficulty} question generated!" - else: - # Style this a bit nicer - difficulty = difficulty.title() - title_text = f"{difficulty} question generated!" - - difficulty = difficulty.title() # If we haven't already (i.e. random question) - - msg_text = f"Here's a new question for you! <{selected_question[1]}|{selected_question[0]}>" - - bot.post_message(command.channel_id, text=title_text, - attachments=[Attachment(SectionBlock(msg_text), color=color)._resolve()]) - - -def select_question(questions: list) -> Tuple[str, str]: - """ - Small helper method that selects a question from a list randomly - """ - if (len(questions) == 0): - return None - return random.choice(questions) - - -def collect_questions(questions: List[Tuple[str, str]], difficulty: str): - """ - Helper method to send GET requests to various Leetcode and HackerRank APIs. - Populates provided dict (urls) with any successfully retrieved data, - in the form of (Question_Title, Question_Link) tuple pairs. - """ - options = [("Hackerrank data structure", HR_DS_API_LINK), - ("Hackerrank Algorithms structure", HR_ALG_API_LINK), - ("Leetcode", LC_API_LINK), - ] - - results = [] - - # Get all the questions off the internet: hr data struct, hr algo, all leetcode - for name, url in options: - try: - results.append((name, requests.get(url, timeout=3))) - except (requests.exceptions.ReadTimeout, requests.exceptions.ConnectTimeout) as error: - print(name + " API timed out!" + "\n" + str(error)) - results.append((name, None)) - - json_blobs: Dict[str, List[Dict]] = {} - - for name, response in results: - if (response is None or response.status_code != HTTPStatus.OK): - if (name != "Leetcode"): - json_blobs["parsed_hr_all"] = json_blobs.get("parsed_hr_all", []) + [] - else: - json_blobs["parsed_lc_all"] = [] - else: - if (name != "Leetcode"): - parsed_hr_data = json.loads(response.text) - json_blobs["parsed_hr_all"] = (json_blobs.get("parsed_hr_all", []) + - parsed_hr_data["models"]) - else: - parsed_lc_data = json.loads(response.text) - json_blobs["parsed_lc_all"] = parsed_lc_data["stat_status_pairs"] - - # Build HackerRank questions tuples from data - for question in json_blobs["parsed_hr_all"]: - # Construct a tuple pair of title for formatting and link to question - question_data = (question["name"], "http://hackerrank.com/challenges/" + question["slug"] - + "/problem") - question_difficulty = question["difficulty_name"].lower() - - # HackerRank annoyingly has 5 difficulty levels, anything above hard is hard - if (question_difficulty == "advanced" or question_difficulty == "expert"): - question_difficulty = "hard" - - if (question_difficulty == difficulty): - questions.append(question_data) - - # Build leetcode question tuples from data, but only the free ones - for question in json_blobs["parsed_lc_all"]: - if (question["paid_only"] is False): - question_data = (question["stat"]["question__title"], "https://leetcode.com/problems/" - + question["stat"]["question__title_slug"] + "/") - - question_difficulty = LC_DIFFICULTY_MAP[question["difficulty"]["level"] - 1] - - if (question_difficulty == difficulty): - questions.append(question_data) diff --git a/unimplemented/meme.py b/unimplemented/meme.py deleted file mode 100644 index bcd0f0bf..00000000 --- a/unimplemented/meme.py +++ /dev/null @@ -1,196 +0,0 @@ -from uqcsbot import bot, Command -import re -from uqcsbot.utils.command_utils import loading_status, success_status, UsageSyntaxException -from urllib.parse import quote - -API_URL = "https://memegen.link/" -# Many different characters need to be replaced in order to work in url format -# See the API_URL for details -REPLACEMENTS = str.maketrans({'_': '__', ' ': '_', '-': '--', '?': '~q', - '%': '~p', '#': '~h', '/': '~s'}) -MEME_NAMES = { - "aag": "Ancient Aliens Guy", - "ackbar": "It's A Trap!", - "afraid": "Afraid to Ask Andy", - "ants": "Do You Want Ants?", - "away": "Life... Finds a Way", - "awesome": "Socially Awesome Penguin", - "awesome-awkward": "Socially Awesome Awkward Penguin", - "awkward": "Socially Awkward Penguin", - "awkward-awesome": "Socially Awkward Awesome Penguin", - "bad": "You Should Feel Bad", - "badchoice": "Milk Was a Bad Choice", - "bd": "Butthurt Dweller", - "bender": "I'm Going to Build My Own Theme Park", - "biw": "Baby Insanity Wolf", - "blb": "Bad Luck Brian", - "boat": "I Should Buy a Boat Cat", - "both": "Why Not Both?", - "bs": "This is Bull, Shark", - "buzz": "X, X Everywhere", - "captain": "I am the Captain Now", - "cb": "Confession Bear", - "cbg": "Comic Book Guy", - "center": "What is this, a Center for Ants?!", - "ch": "Captain Hindsight", - "chosen": "You Were the Chosen One!", - "crazypills": "I Feel Like I'm Taking Crazy Pills", - "cryingfloor": "Crying on Floor", - "disastergirl": "Disaster Girl", - "dodgson": "See? Nobody Cares", - "doge": "Doge", - "drake": "Drakeposting", - "dsm": "Dating Site Murderer", - "dwight": "Schrute Facts", - "elf": "You Sit on a Throne of Lies", - "ermg": "Ermahgerd", - "fa": "Forever Alone", - "facepalm": "Facepalm", - "fbf": "Foul Bachelor Frog", - "fetch": "Stop Trying to Make Fetch Happen", - "fine": "This is Fine", - "firsttry": "First Try!", - "fmr": "Fuck Me, Right?", - "fry": "Futurama Fry", - "fwp": "First World Problems", - "gandalf": "Confused Gandalf", - "ggg": "Good Guy Greg", - "grumpycat": "Grumpy Cat", - "hagrid": "I Should Not Have Said That", - "happening": "It's Happening", - "hipster": "Hipster Barista", - "icanhas": "I Can Has Cheezburger?", - "imsorry": "Oh, I'm Sorry, I Thought This Was America", - "inigo": "Inigo Montoya", - "interesting": "The Most Interesting Man in the World", - "ive": "Jony Ive Redesigns Things", - "iw": "Insanity Wolf", - "jetpack": "Nothing To Do Here", - "joker": "It's Simple, Kill the Batman", - "jw": "Probably Not a Good Idea", - "keanu": "Conspiracy Keanu", - "kermit": "But That's None of My Business", - "live": "Do It Live!", - "ll": "Laughing Lizard", - "mb": "Member Berries", - "mmm": "Minor Mistake Marvin", - "money": "Shut Up and Take My Money!", - "mordor": "One Does Not Simply Walk into Mordor", - "morpheus": "Matrix Morpheus", - "mw": "I Guarantee It", - "nice": "So I Got That Goin' For Me, Which is Nice", - "noidea": "I Have No Idea What I'm Doing", - "oag": "Overly Attached Girlfriend", - "officespace": "That Would Be Great", - "older": "An Older Code Sir, But It Checks Out", - "oprah": "Oprah You Get a Car", - "patrick": "Push it somewhere else Patrick", - "philosoraptor": "Philosoraptor", - "puffin": "Unpopular opinion puffin", - "red": "Oh, Is That What We're Going to Do Today?", - "regret": "I Immediately Regret This Decision!", - "remembers": "Pepperidge Farm Remembers", - "rollsafe": "Roll Safe", - "sad-biden": "Sad Joe Biden", - "sad-boehner": "Sad John Boehner", - "sad-bush": "Sad George Bush", - "sad-clinton": "Sad Bill Clinton", - "sad-obama": "Sad Barack Obama", - "sadfrog": "Sad Frog / Feels Bad Man", - "saltbae": "Salt Bae", - "sarcasticbear": "Sarcastic Bear", - "sb": "Scumbag Brain", - "scc": "Sudden Clarity Clarence", - "sf": "Sealed Fate", - "sk": "Skeptical Third World Kid", - "ski": "Super Cool Ski Instructor", - "snek": "Skeptical Snake", - "soa": "Seal of Approval", - "sohappy": "I Would Be So Happy", - "sohot": "So Hot Right Now", - "sparta": "This is Sparta!", - "spongebob": "Mocking Spongebob", - "ss": "Scumbag Steve", - "stew": "Baby, You've Got a Stew Going", - "success": "Success Kid", - "tenguy": "10 Guy", - "toohigh": "The Rent Is Too Damn High", - "tried": "At Least You Tried", - "ugandanknuck": "Ugandan Knuckles", - "whatyear": "What Year Is It?", - "winter": "Winter is coming", - "wonka": "Condescending Wonka", - "xy": "X all the Y", - "yallgot": "Y'all Got Any More of Them", - "yodawg": "Xzibit Yo Dawg", - "yuno": "Y U NO Guy", -} - -# TODO: Would be really simple to add custom UQCS memes - - -@bot.on_command("meme") -@loading_status -def handle_meme(command: Command): - """ - `!meme "" "")>` - Generates a meme of the given format with the provided top and - bottom text. For a full list of formats, try `!meme names`. - """ - channel = command.channel_id - - if not command.has_arg(): - raise UsageSyntaxException() - - name = command.arg.split()[0].lower() - if name == "names": - send_meme_names(command) - return - elif name not in MEME_NAMES.keys(): - bot.post_message(channel, "The meme name is invalid. " - "Try `!meme names` to get a list of all valid names") - return - - args = get_meme_arguments(command.arg) - if len(args) != 2: - raise UsageSyntaxException() - - # Make an attachment linking to image - top, bottom = args - image_url = API_URL + f"{quote(name)}/{quote(top)}/{quote(bottom)}.jpg" - attachments = [{"text": "", "image_url": image_url}] - bot.post_message(channel, "", attachments=attachments) - - -@success_status -def send_meme_names(command: Command): - """ - Sends the full list of meme names to the users channel to avoid channel spam - """ - user_channel = bot.channels.get(command.user_id) - names_text = "\n".join((f"{full_name}: {name}" for (name, full_name) in MEME_NAMES.items())) - attachments = [{'text': names_text, 'title': "Meme Names:"}] - bot.post_message(user_channel, "", attachments=attachments) - - -def get_meme_arguments(input_args: str): - """ - Gets the top and bottom text and returns them in a - url friendly form that conforms with the api standards - """ - # This gets the text between the quotation marks (and ignores \") - args = re.findall(r'"(.*?(? Tuple[int, str]: - """ - Returns a parking HTML document from the UQ P&F website - """ - page = requests.get("https://pg.pf.uq.edu.au/") - return (page.status_code, page.text) - - -@bot.on_command("parking") -@loading_status -def handle_parking(command: Command) -> None: - """ - `!parking [all]` - Displays how many car parks are available at UQ St. Lucia - By default, only dispalys casual parking availability - """ - - if command.has_arg() and command.arg.lower() == "all": - permit = True - else: - permit = False - - # read parking data - code, data = get_pf_parking_data() - if code != 200: - bot.post_message(command.channel_id, "Could Not Retrieve Parking Data") - return - - response = ["*Available Parks at UQ St. Lucia*"] - names = {"P1": "P1 - Warehouse (14P Daily)", "P2": "P2 - Space Bank (14P Daily)", - "P3": "P3 - Multi-Level West (Staff)", "P4": "P4 - Multi-Level East (Staff)", - "P6": "P6 - Hartley Teakle (14P Hourly)", "P7": "P7 - DustBowl (14P Daily)", - "P7 UC": "P7 - Keith Street (14P Daily Capped)", - "P8 L1": "P8 - Athletics Basement (14P Daily)", - "P8 L2": "P8 - Athletics Roof (14P Daily)", "P9": "P9 - Boatshed (14P Daily)", - "P10": "P10 - UQ Centre & Playing Fields (14P Daily/14P Daily Capped)", - "P11 L1": "P11 - Conifer Knoll Lower (Staff)", - "P11 L2": "P11 - Conifer Knoll Upper (Staff)", - "P11 L3": "P11 - Conifer Knoll Roof (14P Daily Restricted)"} - - def category(fill): - if fill.upper() == "FULL": - return "No" - if fill.upper() == "NEARLY FULL": - return "Few" - return fill - - # find parks - table = Soup(data, "html.parser").find("table", attrs={"id": "parkingAvailability"}) - rows = table.find_all("tr")[1:] - # split and join for single space whitespace - areas = [[" ".join(i.get_text().split()) for i in j.find_all("td")] for j in rows] - - for area in areas: - if area[2]: - response.append(f"{category(area[2])} Carparks Available in {names[area[0]]}") - elif permit and area[1]: - response.append(f"{category(area[1])} Carparks Available in {names[area[0]]}") - bot.post_message(command.channel_id, "\n".join(response)) diff --git a/unimplemented/pokemash.py b/unimplemented/pokemash.py deleted file mode 100644 index caaf6cd6..00000000 --- a/unimplemented/pokemash.py +++ /dev/null @@ -1,503 +0,0 @@ -from uqcsbot import bot, Command -from re import match -from typing import Optional - -POKEDEX = {"bulbasaur": 1, - "ivysaur": 2, - "venusaur": 3, - "charmander": 4, - "charmeleon": 5, - "charizard": 6, - "squirtle": 7, - "wartortle": 8, - "blastoise": 9, - "caterpie": 10, - "metapod": 11, - "butterfree": 12, - "weedle": 13, - "kakuna": 14, - "beedrill": 15, - "pidgey": 16, - "pidgeotto": 17, - "pidgeot": 18, - "rattata": 19, - "raticate": 20, - "spearow": 21, - "fearow": 22, - "ekans": 23, - "arbok": 24, - "pikachu": 25, - "raichu": 26, - "sandshrew": 27, - "sandslash": 28, - "nidoran(f)": 29, - "nidorina": 30, - "nidoqueen": 31, - "nidoran(m)": 32, - "nidorino": 33, - "nidoking": 34, - "clefairy": 35, - "clefable": 36, - "vulpix": 37, - "ninetales": 38, - "jigglypuff": 39, - "wigglytuff": 40, - "zubat": 41, - "golbat": 42, - "oddish": 43, - "gloom": 44, - "vileplume": 45, - "paras": 46, - "parasect": 47, - "venonat": 48, - "venomoth": 49, - "diglett": 50, - "dugtrio": 51, - "meowth": 52, - "persian": 53, - "psyduck": 54, - "golduck": 55, - "mankey": 56, - "primeape": 57, - "growlithe": 58, - "arcanine": 59, - "poliwag": 60, - "poliwhirl": 61, - "poliwrath": 62, - "abra": 63, - "kadabra": 64, - "alakazam": 65, - "machop": 66, - "machoke": 67, - "machamp": 68, - "bellsprout": 69, - "weepinbell": 70, - "victreebel": 71, - "tentacool": 72, - "tentacruel": 73, - "geodude": 74, - "graveler": 75, - "golem": 76, - "ponyta": 77, - "rapidash": 78, - "slowpoke": 79, - "slowbro": 80, - "magnemite": 81, - "magneton": 82, - "farfetchd": 83, - "doduo": 84, - "dodrio": 85, - "seel": 86, - "dewgong": 87, - "grimer": 88, - "muk": 89, - "shellder": 90, - "cloyster": 91, - "gastly": 92, - "haunter": 93, - "gengar": 94, - "onix": 95, - "drowzee": 96, - "hypno": 97, - "krabby": 98, - "kingler": 99, - "voltorb": 100, - "electrode": 101, - "exeggcute": 102, - "exeggutor": 103, - "cubone": 104, - "marowak": 105, - "hitmonlee": 106, - "hitmonchan": 107, - "lickitung": 108, - "koffing": 109, - "weezing": 110, - "rhyhorn": 111, - "rhydon": 112, - "chansey": 113, - "tangela": 114, - "kangaskhan": 115, - "horsea": 116, - "seadra": 117, - "goldeen": 118, - "seaking": 119, - "staryu": 120, - "starmie": 121, - "mr. mime": 122, - "scyther": 123, - "jynx": 124, - "electabuzz": 125, - "magmar": 126, - "pinsir": 127, - "tauros": 128, - "magikarp": 129, - "gyarados": 130, - "lapras": 131, - "ditto": 132, - "eevee": 133, - "vaporeon": 134, - "jolteon": 135, - "flareon": 136, - "porygon": 137, - "omanyte": 138, - "omastar": 139, - "kabuto": 140, - "kabutops": 141, - "aerodactyl": 142, - "snorlax": 143, - "articuno": 144, - "zapdos": 145, - "moltres": 146, - "dratini": 147, - "dragonair": 148, - "dragonite": 149, - "mewtwo": 150, - "mew": 151} - -PREFIX = {1: "Bulb", - 2: "Ivy", - 3: "Venu", - 4: "Char", - 5: "Char", - 6: "Char", - 7: "Squirt", - 8: "War", - 9: "Blast", - 10: "Cater", - 11: "Meta", - 12: "Butter", - 13: "Wee", - 14: "Kak", - 15: "Bee", - 16: "Pid", - 17: "Pidg", - 18: "Pidg", - 19: "Rat", - 20: "Rat", - 21: "Spear", - 22: "Fear", - 23: "Ek", - 24: "Arb", - 25: "Pika", - 26: "Rai", - 27: "Sand", - 28: "Sand", - 29: "Nido", - 30: "Nido", - 31: "Nido", - 32: "Nido", - 33: "Nido", - 34: "Nido", - 35: "Clef", - 36: "Clef", - 37: "Vul", - 38: "Nine", - 39: "Jiggly", - 40: "Wiggly", - 41: "Zu", - 42: "Gol", - 43: "Odd", - 44: "Gloo", - 45: "Vile", - 46: "Pa", - 47: "Para", - 48: "Veno", - 49: "Veno", - 50: "Dig", - 51: "Dug", - 52: "Meow", - 53: "Per", - 54: "Psy", - 55: "Gol", - 56: "Man", - 57: "Prime", - 58: "Grow", - 59: "Arca", - 60: "Poli", - 61: "Poli", - 62: "Poli", - 63: "Ab", - 64: "Kada", - 65: "Ala", - 66: "Ma", - 67: "Ma", - 68: "Ma", - 69: "Bell", - 70: "Weepin", - 71: "Victree", - 72: "Tenta", - 73: "Tenta", - 74: "Geo", - 75: "Grav", - 76: "Gol", - 77: "Pony", - 78: "Rapi", - 79: "Slow", - 80: "Slow", - 81: "Magne", - 82: "Magne", - 83: "Far", - 84: "Do", - 85: "Do", - 86: "See", - 87: "Dew", - 88: "Gri", - 89: "Mu", - 90: "Shell", - 91: "Cloy", - 92: "Gas", - 93: "Haunt", - 94: "Gen", - 95: "On", - 96: "Drow", - 97: "Hyp", - 98: "Krab", - 99: "King", - 100: "Volt", - 101: "Electr", - 102: "Exegg", - 103: "Exegg", - 104: "Cu", - 105: "Maro", - 106: "Hitmon", - 107: "Hitmon", - 108: "Licki", - 109: "Koff", - 110: "Wee", - 111: "Rhy", - 112: "Rhy", - 113: "Chan", - 114: "Tang", - 115: "Kangas", - 116: "Hors", - 117: "Sea", - 118: "Gold", - 119: "Sea", - 120: "Star", - 121: "Star", - 122: "Mr.", - 123: "Scy", - 124: "Jyn", - 125: "Electa", - 126: "Mag", - 127: "Pin", - 128: "Tau", - 129: "Magi", - 130: "Gyara", - 131: "Lap", - 132: "Dit", - 133: "Ee", - 134: "Vapor", - 135: "Jolt", - 136: "Flare", - 137: "Pory", - 138: "Oma", - 139: "Oma", - 140: "Kabu", - 141: "Kabu", - 142: "Aero", - 143: "Snor", - 144: "Artic", - 145: "Zap", - 146: "Molt", - 147: "Dra", - 148: "Dragon", - 149: "Dragon", - 150: "Mew", - 151: "Mew"} - -SUFFIX = {1: "basaur", - 2: "ysaur", - 3: "usaur", - 4: "mander", - 5: "meleon", - 6: "izard", - 7: "tle", - 8: "tortle", - 9: "toise", - 10: "pie", - 11: "pod", - 12: "free", - 13: "dle", - 14: "una", - 15: "drill", - 16: "gey", - 17: "eotto", - 18: "eot", - 19: "tata", - 20: "icate", - 21: "row", - 22: "row", - 23: "kans", - 24: "bok", - 25: "chu", - 26: "chu", - 27: "shrew", - 28: "slash", - 29: "oran", - 30: "rina", - 31: "queen", - 32: "ran", - 33: "rino", - 34: "king", - 35: "fairy", - 36: "fable", - 37: "pix", - 38: "tales", - 39: "puff", - 40: "tuff", - 41: "bat", - 42: "bat", - 43: "ish", - 44: "oom", - 45: "plume", - 46: "ras", - 47: "sect", - 48: "nat", - 49: "moth", - 50: "lett", - 51: "trio", - 52: "th", - 53: "sian", - 54: "duck", - 55: "duck", - 56: "key", - 57: "ape", - 58: "lithe", - 59: "nine", - 60: "wag", - 61: "whirl", - 62: "wrath", - 63: "ra", - 64: "bra", - 65: "kazam", - 66: "chop", - 67: "choke", - 68: "champ", - 69: "sprout", - 70: "bell", - 71: "bell", - 72: "cool", - 73: "cruel", - 74: "dude", - 75: "eler", - 76: "em", - 77: "ta", - 78: "dash", - 79: "poke", - 80: "bro", - 81: "mite", - 82: "ton", - 83: "fetchd", - 84: "duo", - 85: "drio", - 86: "eel", - 87: "gong", - 88: "mer", - 89: "uk", - 90: "der", - 91: "ster", - 92: "tly", - 93: "ter", - 94: "gar", - 95: "ix", - 96: "zee", - 97: "no", - 98: "by", - 99: "ler", - 100: "orb", - 101: "ode", - 102: "cute", - 103: "utor", - 104: "bone", - 105: "wak", - 106: "lee", - 107: "chan", - 108: "tung", - 109: "fing", - 110: "zing", - 111: "horn", - 112: "don", - 113: "sey", - 114: "gela", - 115: "khan", - 116: "sea", - 117: "dra", - 118: "deen", - 119: "king", - 120: "yu", - 121: "mie", - 122: "mime", - 123: "ther", - 124: "nx", - 125: "buzz", - 126: "mar", - 127: "sir", - 128: "ros", - 129: "karp", - 130: "dos", - 131: "ras", - 132: "to", - 133: "vee", - 134: "eon", - 135: "eon", - 136: "eon", - 137: "gon", - 138: "nyte", - 139: "star", - 140: "to", - 141: "tops", - 142: "dactyl", - 143: "lax", - 144: "cuno", - 145: "dos", - 146: "tres", - 147: "tini", - 148: "nair", - 149: "nite", - 150: "two", - 151: "ew"} - - -def lookup(command: Command, arg: str) -> Optional[int]: - """ - converts a string representing a pokemon's name or number to an integer - """ - try: - num = int(arg) - except ValueError: - if arg not in POKEDEX: - bot.post_message(command.channel_id, f"Could Not Find Pokemon: {arg}") - return None - num = POKEDEX[arg] - if num <= 0 or num > 151: - bot.post_message(command.channel_id, f"Out of Range: {arg}") - return None - return num - - -@bot.on_command('pokemash') -def handle_pokemash(command: Command): - """ - `!pokemash pokemon pokemon` - Returns the pokemash of the two Pokemon. - Can use Pokemon names or Pokedex numbers (first gen only) - """ - cmd = command.arg.lower() - # checks for exactly two pokemon - # mr. mime is the only pokemon with a space in it's name - if not cmd or (cmd.count(" ") - cmd.count("mr. mime")) != 1: - bot.post_message(command.channel_id, "Incorrect Number of Pokemon") - return - - # two pokemon split - arg_left, arg_right = match(r"(mr\. mime|\S+) (mr\. mime|\S+)", cmd).group(1, 2) - - num_left = lookup(command, arg_left) - num_right = lookup(command, arg_right) - if num_left is None or num_right is None: - return - - bot.post_message(command.channel_id, - f"_{PREFIX[num_left]+SUFFIX[num_right]}_\n" - f"https://images.alexonsager.net/pokemon/fused/" - + f"{num_right}/{num_right}.{num_left}.png") diff --git a/unimplemented/radar.py b/unimplemented/radar.py deleted file mode 100644 index f5e44ca1..00000000 --- a/unimplemented/radar.py +++ /dev/null @@ -1,14 +0,0 @@ -from uqcsbot import bot, Command -from time import time - - -@bot.on_command("radar") -def handle_radar(command: Command): - """ - `!radar` - Returns the latest BOM radar image for Brisbane. - """ - time_in_s = int(time()) - radar_url = f'https://bom.lambda.tools/?location=brisbane×tamp={time_in_s}' - attachment = {"image_url": radar_url, "text": None, - "fallback": "Screenshot from the Bureau of Meterology's Brisbane radar."} - bot.post_message(command.channel_id, radar_url, attachments=[attachment]) diff --git a/unimplemented/spider.py b/unimplemented/spider.py deleted file mode 100644 index 10e40e42..00000000 --- a/unimplemented/spider.py +++ /dev/null @@ -1,9 +0,0 @@ -from uqcsbot import bot, Command - - -@bot.on_command("spider") -def handle_spider(command: Command): - """ - `!spider` - Displays the spider shrug. - """ - bot.post_message(command.channel_id, "//\\; ;/\\\\") diff --git a/unimplemented/techcrunch.py b/unimplemented/techcrunch.py deleted file mode 100644 index fbee3067..00000000 --- a/unimplemented/techcrunch.py +++ /dev/null @@ -1,48 +0,0 @@ -from uqcsbot import bot, Command -from uqcsbot.utils.command_utils import loading_status -from typing import Tuple, Dict, List - -import feedparser - -ARTICLES_TO_POST = 5 -RSS_URL = "http://feeds.feedburner.com/TechCrunch/" -TECHCRUNCH_URL = "https://techcrunch.com" - -def get_tech_crunch_data() -> List[Dict[str, str]]: - """ - Returns data from TechCrunch RSS feed - """ - data = feedparser.parse(RSS_URL) - if data.status != 200: - return None - return data.entries - -def get_data_from_article(news: List[Dict[str, str]], index: int) -> Tuple[str, str]: - """ - Returns the title of the article and the link - - Tuple returned: (title, url) - """ - return (news[index]['title'], news[index]['link']) - -@bot.on_command("techcrunch") -@loading_status -def handle_news(command: Command) -> None: - """ - Prints the 5 top-most articles in the Latest News Section of TechCrunch - using RSS feed - """ - message = f"*Latest News from <{TECHCRUNCH_URL}|_TechCrunch_> :techcrunch:*\n" - news = get_tech_crunch_data() - if news is None: - bot.post_message(command.channel_id, "There was an error accessing " - "TechCrunch RSS feed") - return - for i in range(ARTICLES_TO_POST): - title, url = get_data_from_article(news, i) - # Formats message a clickable headline which links to the article - # These articles are also now bullet pointed - message += f"• <{url}|{title}>\n" - # Additional parameters ensure that the links don't show as big articles - # underneath the input - bot.post_message(command.channel_id, message, unfurl_links=False, unfurl_media=False) diff --git a/unimplemented/trivia.py b/unimplemented/trivia.py deleted file mode 100644 index f7fda40d..00000000 --- a/unimplemented/trivia.py +++ /dev/null @@ -1,376 +0,0 @@ -import argparse -import base64 -import json -import random -from datetime import datetime, timezone, timedelta -from functools import partial -from typing import List, Dict, Union, NamedTuple, Optional, Callable, Set - -import requests - -from uqcsbot import bot, Command -from uqcsbot.api import Channel -from uqcsbot.utils.command_utils import loading_status, UsageSyntaxException - -API_URL = "https://opentdb.com/api.php" -CATEGORIES_URL = "https://opentdb.com/api_category.php" - -# NamedTuple for use with the data returned from the api -QuestionData = NamedTuple('QuestionData', - [('type', str), ('question', str), ('correct_answer', str), - ('answers', List[str]), ('is_boolean', bool)]) - -# Contains information about a reaction and the list of users who used said reaction -ReactionUsers = NamedTuple('ReactionUsers', [('name', str), ('users', Set[str])]) - -# Customisation options -# The interval between reactions being made for the possible answers (prevents order changing) -REACT_INTERVAL = 1 -MIN_SECONDS = 5 -MAX_SECONDS = 300 - -# The channels where multiple trivia questions can be asked (prevent spam) -VALID_SEQUETIAL_CHANNELS = ['trivia', 'bot-testing'] -MAX_SEQUENTIAL_QUESTIONS = 30 - -BOOLEAN_REACTS = ['this', 'not-this'] # Format of [ , ] -# Colours should match CHOICE_COLORS -MULTIPLE_CHOICE_REACTS = ['green_heart', 'yellow_heart', 'heart', 'blue_heart'] -CHOICE_COLORS = ['#6C9935', '#F3C200', '#B6281E', '#3176EF'] - -# What arguments to use for the cron job version -CRON_CHANNEL = 'trivia' -# (One day - 15 seconds) Overrides any -s argument below and ignores MAX_SECONDS rule -CRON_SECONDS = 86385 -CRON_ARGUMENTS = '' - - -@bot.on_command('trivia') -@loading_status -def handle_trivia(command: Command): - """ - `!trivia [-d ] [-c ] - [-t ] [-s ] [-n ] [--cats]` - - Asks a new trivia question - """ - args = parse_arguments(command.channel_id, command.arg if command.has_arg() else '') - - # End early if the help option was used - if args.help: - return - - # Send the possible categories - if args.cats: - bot.post_message(command.channel_id, get_categories()) - return - - # Check if the channel is valid for sequential questions - current_channel = bot.channels.get(command.channel_id) - if all([args.count > 1, not current_channel.is_im, - current_channel.name not in VALID_SEQUETIAL_CHANNELS]): - # If no valid channels are specified - if len(VALID_SEQUETIAL_CHANNELS) == 0: - bot.post_message(command.channel_id, - 'This command can only be used in private messages with the bot') - return - - first_valid = bot.channels.get(VALID_SEQUETIAL_CHANNELS[0]) - channel_message = '' - if first_valid: - channel_message = f'Try <#{first_valid.id}|{VALID_SEQUETIAL_CHANNELS[0]}>.' - bot.post_message(command.channel_id, f'You cannot use the sequential questions ' - + f'feature in this channel. {channel_message}') - return - - handle_question(command.channel_id, args) - - -def parse_arguments(channel: Channel, arg_string: str) -> argparse.Namespace: - """ - Parses the arguments for the command - :param command: The command which the handle_trivia function receives - :return: An argpase Namespace object with the parsed arguments - """ - parser = argparse.ArgumentParser(prog='!trivia', add_help=False) - - def usage_error(*args, **kwargs): - raise UsageSyntaxException() - - parser.error = usage_error # type: ignore - parser.add_argument('-d', '--difficulty', choices=['easy', 'medium', 'hard'], - default='random', type=str.lower, - help='The difficulty of the question. (default: %(default)s)') - parser.add_argument('-c', '--category', default=-1, type=int, - help='Specifies a category (default: any)') - parser.add_argument('-t', '--type', choices=['boolean', 'multiple'], - default="random", type=str.lower, - help='The type of question. (default: %(default)s)') - parser.add_argument('-s', '--seconds', default=30, type=int, - help='Number of seconds before posting answer (default: %(default)s)') - parser.add_argument('-n', '--count', default=1, type=int, help=f"Do 'n' trivia questions in " - f"quick succession (max : {MAX_SEQUENTIAL_QUESTIONS})") - parser.add_argument('--cats', action='store_true', - help='Sends a list of valid categories to the user') - parser.add_argument('-h', '--help', action='store_true', help='Prints this help message') - - args = parser.parse_args(arg_string.split()) - - # If the help option was used print the help message to - # the channel (needs access to the parser to do this) - if args.help: - bot.post_message(channel, parser.format_help()) - - # Constrain the number of seconds to a reasonable frame - args.seconds = max(MIN_SECONDS, args.seconds) - args.seconds = min(args.seconds, MAX_SECONDS) - - # Constrain the number of sequential questions - args.count = max(args.count, 1) - args.count = min(args.count, MAX_SEQUENTIAL_QUESTIONS) - - # Add an original count to keep track - args.original_count = args.count - - return args - - -def get_categories() -> str: - """ - Gets the message to send if the user wants a list of the available categories. - """ - http_response = requests.get(CATEGORIES_URL) - if http_response.status_code != requests.codes.ok: - return "There was a problem getting the response" - - categories = json.loads(http_response.content)['trivia_categories'] - - # Construct pretty results to print in a code block to avoid a large spammy message - pretty_results = '```Use the id to specify a specific category \n\nID Name\n' - - for category in categories: - pretty_results += f'{category["id"]:<4d}{category["name"]}\n' - - pretty_results += '```' - - return pretty_results - - -def handle_question(channel: Channel, args: argparse.Namespace): - """ - Handles getting a question and posting it to the channel as well as scheduling the answer. - Returns the reaction string for the correct answer. - """ - question_data = get_question_data(channel, args) - - if question_data is None: - return - - question_number = args.original_count - args.count + 1 - prefix = f'Q{question_number}:' if args.original_count > 1 else '' - post_question(channel, question_data, prefix) - - # Get the answer message - if question_data.is_boolean: - if question_data.correct_answer == 'True': - answer_text = f':{BOOLEAN_REACTS[0]}:' - else: - answer_text = f':{BOOLEAN_REACTS[1]}:' - else: - answer_text = question_data.correct_answer - - answer_message = f'The answer to the question *{question_data.question}* is: *{answer_text}*' - - # Schedule the answer to be posted after the specified number of seconds has passed - post_answer = partial(bot.post_message, channel, answer_message) - schedule_action(post_answer, args.seconds) - - # If more questions are to be asked schedule the question for 5 seconds after the current answer - if args.count > 1: - args.count -= 1 - schedule_action(partial(handle_question, channel, args), args.seconds + 5) - - -def get_question_data(channel: Channel, args: argparse.Namespace) -> Optional[QuestionData]: - """ - Attempts to get a question from the api using the specified arguments. - Returns the dictionary object for the question on success - and None on failure (after posting an error message). - """ - # Base64 to help with encoding the message for slack - params: Dict[str, Union[int, str]] = {'amount': 1, 'encode': 'base64'} - - # Add in any explicitly specified arguments - if args.category != -1: - params['category'] = args.category - - if args.difficulty != 'random': - params['difficulty'] = args.difficulty - - if args.type != 'random': - params['type'] = args.type - - # Get the response and check that it is valid - http_response = requests.get(API_URL, params=params) - if http_response.status_code != requests.codes.ok: - bot.post_message(channel, "There was a problem getting the response") - return None - - # Check the response codes and post a useful message in the case of an error - response_content = json.loads(http_response.content) - if response_content['response_code'] == 2: - bot.post_message(channel, "Invalid category id. " - + "Try !trivia --cats for a list of valid categories.") - return None - elif response_content['response_code'] != 0: - bot.post_message(channel, "No results were returned") - return None - - question_data = response_content['results'][0] - - # Get the type of question and make the NamedTuple container for the data - is_boolean = len(question_data['incorrect_answers']) == 1 - answers = [question_data['correct_answer']] + question_data['incorrect_answers'] - - # Delete the ones we don't need - del question_data['category'] - del question_data['difficulty'] - del question_data['incorrect_answers'] - - # Decode the ones we want. The base 64 decoding ensures - # that the formatting works properly with slack. - question_data['question'] = decode_b64(question_data['question']) - question_data['correct_answer'] = decode_b64(question_data['correct_answer']) - answers = [decode_b64(ans) for ans in answers] - - question_data = QuestionData(is_boolean=is_boolean, answers=answers, **question_data) - - # Shuffle the answers - random.shuffle(question_data.answers) - - return question_data - - -def post_question(channel: Channel, question_data: QuestionData, prefix: str = '') -> float: - """ - Posts the question from the given QuestionData along with - the possible answers list if applicable. - Also creates the answer reacts. - Returns the timestamp of the posted message. - """ - # Post the question and get the timestamp for the reactions (asterisks bold it) - message_ts = bot.post_message(channel, f'*{prefix} {question_data.question}*')['ts'] - - # Print the questions (if multiple choice) and add the answer reactions - reactions = BOOLEAN_REACTS if question_data.is_boolean else MULTIPLE_CHOICE_REACTS - - if not question_data.is_boolean: - message_ts = post_possible_answers(channel, question_data.answers) - - add_reactions_interval(reactions, channel, message_ts, REACT_INTERVAL) - - return message_ts - - -def add_reactions_interval(reactions: List[str], channel: Channel, - msg_timestamp: str, interval: float = 1): - """ - Adds the given reactions with "interval" seconds between in order - to prevent them from changing order in slack (as slack uses the - timestamp of when the reaction was added to determine the order). - :param reactions: The reactions to add - :param channel: The channel containing the desired message to react to - :param msg_timestamp: The timestamp of the required message - :param interval: The interval between posting each reaction (defaults to 1 second) - """ - # If the react interval is 0 don't do any of the scheduling stuff - if REACT_INTERVAL == 0: - for reaction in reactions: - bot.api.reactions.add(name=reaction, channel=channel, timestamp=msg_timestamp) - - return - - # Do the first one immediately - bot.api.reactions.add(name=reactions[0], channel=channel, timestamp=msg_timestamp) - - # I am not 100% sure why this is needed. Doing it with a normal partial or - # lambda will try to post the same reacts - def add_reaction(reaction: str): - bot.api.reactions.add(name=reaction, channel=channel, timestamp=msg_timestamp) - - for index, reaction in enumerate(reactions[1:]): - delay = (index + 1) * interval - schedule_action(partial(add_reaction, reaction), delay) - - -def decode_b64(encoded: str) -> str: - """ - Takes a base64 encoded string. Returns the decoded version to utf-8. - """ - return base64.b64decode(encoded).decode('utf-8') - - -def get_correct_reaction(question_data: QuestionData): - """ - Returns the reaction that matches with the correct answer - """ - if question_data.is_boolean: - if question_data.correct_answer == 'True': - correct_reaction = BOOLEAN_REACTS[0] - else: - correct_reaction = BOOLEAN_REACTS[1] - else: - correct_reaction = MULTIPLE_CHOICE_REACTS[ - question_data.answers.index(question_data.correct_answer)] - - return correct_reaction - - -def post_possible_answers(channel: Channel, answers: List[str]) -> float: - """ - Posts the possible answers for a multiple choice question in a nice way. - Returns the timestamp of the message to allow reacting to it. - """ - attachments = [] - for col, answer in zip(CHOICE_COLORS, answers): - ans_att = {'text': answer, 'color': col} - attachments.append(ans_att) - - return bot.post_message(channel, '', attachments=attachments)['ts'] - - -def schedule_action(action: Callable, secs: Union[int, float]): - """ - Schedules the supplied action to be called once in the given number of seconds. - """ - run_date = datetime.now(timezone(timedelta(hours=10))) + timedelta(seconds=secs) - bot._scheduler.add_job(action, 'date', run_date=run_date) - - -@bot.on_schedule('cron', hour=12, timezone='Australia/Brisbane') -def daily_trivia(): - """ - Adds a job that displays a random question to the specified channel at lunch time - """ - channel = bot.channels.get(CRON_CHANNEL).id - - # Get arguments and update the seconds - args = parse_arguments(channel, CRON_ARGUMENTS) - args.seconds = CRON_SECONDS - - # Get and post the actual question - handle_question(channel, args) - - # Format a nice message to tell when the answer will be - hours = CRON_SECONDS // 3600 - minutes = (CRON_SECONDS - (hours * 3600)) // 60 - if minutes > 55: - hours += 1 - minutes = 0 - - time_until_answer = 'Answer in ' - if hours > 0: - time_until_answer += f'{hours} hours' - if minutes > 0: - time_until_answer += f' and {minutes} minutes' if hours > 0 else f'{minutes} minutes' - - bot.post_message(channel, time_until_answer) diff --git a/unimplemented/umart.py b/unimplemented/umart.py deleted file mode 100644 index 0daaa2f5..00000000 --- a/unimplemented/umart.py +++ /dev/null @@ -1,79 +0,0 @@ -from uqcsbot import bot, Command -from requests import get -from requests.exceptions import RequestException -from bs4 import BeautifulSoup -from uqcsbot.utils.command_utils import loading_status - -NO_QUERY_MESSAGE = "You can't look for nothing. `!umart `" -NO_RESULTS_MESSAGE = "I can't find nothing baus! Try `!umart `" -ERROR_MESSAGE = "I tried to get the things but alas I could not. Error with HTTP Request." - -UMART_SEARCH_URL = "https://www.umart.com.au/umart1/pro/products_list_searchnew_min.phtml" -UMART_PRODUCT_URL = "https://www.umart.com.au/umart1/pro/" - - -@bot.on_command("umart") -@loading_status -def handle_umart(command: Command): - """ - `!umart ` - Returns 5 top results for products from umart matching the search query. - """ - # Makes sure the query is not empty - if not command.has_arg(): - bot.post_message(command.channel_id, NO_QUERY_MESSAGE) - return - search_query = command.arg.strip() - # Detects if user is being smart - if "SOMETHING NOT AS SPECIFIC" in search_query: - bot.post_message(command.channel_id, "Not literally...") - return - search_results = get_umart_results(search_query) - if search_results is None: - bot.post_message(command.channel_id, ERROR_MESSAGE) - return - if len(search_results) == 0: - bot.post_message(command.channel_id, NO_RESULTS_MESSAGE) - return - message = "```" - for result in search_results: - message += f"Name: <{UMART_PRODUCT_URL}{result['link']}|{result['name']}>\n" - message += f"Price: {result['price']}\n" - message += "```" - bot.post_message(command.channel_id, message) - - -def get_umart_results(search_query): - """ - Gets the top results based on the search_query. - Returns up to 5 results. - """ - search_page = get_search_page(search_query) - if search_page is None: - return None - return get_results_from_page(search_page) - - -def get_results_from_page(search_page): - """ - Strips results from html page - """ - html = BeautifulSoup(search_page, "html.parser") - search_items = [] - for li in html.select("li"): - name = li.select("a.proname")[0].get_text() - price = li.select("dl:nth-of-type(2) > dd > span")[0].get_text() - link = li.select("a.proname")[0]["href"] - search_items.append({"name": name, "price": price, "link": link}) - return search_items - - -def get_search_page(search_query): - """ - Gets the search page HTML - """ - try: - resp = get(UMART_SEARCH_URL + "?search=" + search_query + "&bid=2") - return resp.content - except RequestException as e: - bot.logger.error(f"A request error {e.resp.status} occurred:\n{e.content}") - return None diff --git a/unimplemented/uqfinal.py b/unimplemented/uqfinal.py deleted file mode 100644 index 6a10c06c..00000000 --- a/unimplemented/uqfinal.py +++ /dev/null @@ -1,144 +0,0 @@ -from math import ceil -from uqcsbot import bot, Command -from requests import get, RequestException, Response -from typing import List -from uqcsbot.utils.command_utils import loading_status - -UQFINAL_API = "https://api.uqfinal.com" -GRADES = [(0.5, 'four'), (0.65, 'five'), (0.75, 'six'), (0.85, 'seven')] - - -@bot.on_command("uqfinal") -@loading_status -def handle_uqfinal(command: Command): - """ - `!uqfinal ` - Check UQFinal for course CODE - with the first assessments pieces as as percentages - """ - # Makes sure the query is not empty - if not command.has_arg(): - bot.post_message(command.channel_id, "Please choose a course") - return - - args = command.arg.split() - - course = args[0] # Always exists - arg_scores = args[1:] - scores: List[float] = [] - - # get UQ Final data - semester = get_uqfinal_semesters() - if semester is None: - bot.post_message(command.channel_id, "Failed to retrieve semester data from UQfinal") - return - - course_info = get_uqfinal_course(semester, course) - if course_info is None: - bot.post_message(command.channel_id, f"Failed to retrieve course information for {course}") - return - assessments = course_info["assessment"] - - # if no results submitted - if not arg_scores: - message = [f"{course.upper()} has the following assessments:"] - for i, assess in enumerate(assessments): - message.append(f"{i+1}: {assess['taskName']} ({assess['weight']}%)") - message.append("_Powered by http://uqfinal.com_") - bot.post_message(command.channel_id, "\n".join(message)) - return - - # convert arugments to decimals - for arg_score in arg_scores: - try: - score = float(arg_score.rstrip("%")) - except ValueError: - bot.post_message(command.channel_id, - f"\"{arg_score}\" could not be converted to a number.") - return - score_deci = score / (100 if score > 1 else 1) - if score_deci < 0 or score_deci > 1: - bot.post_message(command.channel_id, - "Assessments scores should be between 0% and 100%.") - return - scores.append(score_deci) - - # if too many results - if len(scores) >= len(assessments): - bot.post_message(command.channel_id, - f"Too many retults provided.\n" - f"This course has {len(assessments)} assessments.") - return - - # calculate achived marks - total_deci = 0.0 - results = [] - for i, score_deci in enumerate(scores): - total_deci += score_deci * float(assessments[i]["weight"]) / 100 - results.append(f"Inputted score of {round(score_deci * 100)}% for" - + f" {assessments[i]['taskName']} (weighted {assessments[i]['weight']}%)") - bot.post_message(command.channel_id, "\n".join(results)) - - # calculate remaining marks - remain_deci = 0.0 - for i in range(len(scores), len(assessments)): - remain_deci += float(assessments[i]["weight"]) / 100 - - # calculate marks needed to achieve grades - message = [] - for cutoff_deci, grade in GRADES: - needed_perc = ceil(100 * (cutoff_deci - total_deci) / remain_deci) - if needed_perc > 100: - break - if needed_perc <= 0: - message.append(f"You have achieved a {grade} :toot:.") - elif len(scores) == len(assessments) - 1: - message.append(f"You need to score at least {needed_perc}%" - + f" on the {assessments[-1]['taskName']} to achieve a {grade}.") - else: - message.append(f"You need to score at least a weighted average of {needed_perc}%" - + f" on the remaining {len(assessments) - len(scores)}" - + f" assessments to achieve a {grade}.") - - # if getting a four impossible - if not message: - message.append("I am a servant of the Secret Fire, wielder of the flame of Anor." - + " The dark fire will not avail you, flame of Udûn." - + " Go back to the Shadow! *You cannot pass.*") - message.append("_Disclaimer: this does not take hurdles into account._") - message.append("_Powered by http://uqfinal.com_") - bot.post_message(command.channel_id, "\n".join(message)) - - -def get_uqfinal_semesters(): - """ - Get the current semester data from uqfinal - Return None on failure - """ - try: - # Assume current semester - semester_response: Response = get(UQFINAL_API + "/semesters") - if semester_response.status_code != 200: - bot.logger.error(f"UQFinal returned {semester_response.status_code}" - + f" when getting the current semester") - return None - return semester_response.json()["data"]["semesters"].pop() - except RequestException as e: - bot.logger.error(f"A request error {e.response.status} occurred:\n{e.response.content}") - return None - - -def get_uqfinal_course(semester, course: str): - """ - Get the current course data from uqfinal - Return None on failure - """ - try: - course_response = get("/".join([UQFINAL_API, "course", str(semester["uqId"]), course])) - if course_response.status_code != 200: - bot.logger.error(f"UQFinal returned {course_response.status_code}" - + f" when getting the course {course}") - return None - return course_response.json()["data"] - except RequestException as e: - bot.logger.error(f"A request error {e.response.status} occurred:\n{e.response.content}") - return None diff --git a/unimplemented/urban.py b/unimplemented/urban.py deleted file mode 100644 index 32e9bcdc..00000000 --- a/unimplemented/urban.py +++ /dev/null @@ -1,62 +0,0 @@ -import re -import requests -from uqcsbot import bot, Command -from uqcsbot.utils.command_utils import loading_status, UsageSyntaxException - -URBAN_API_ENDPOINT = 'http://api.urbandictionary.com/v0/define' -URBAN_USER_ENDPOINT = 'https://www.urbandictionary.com/define.php' - - -@bot.on_command('urban') -@loading_status -def handle_urban(command: Command) -> None: - """ - `!urban ` - Looks a phrase up on Urban Dictionary. - """ - # Check for search phase - if not command.has_arg(): - raise UsageSyntaxException() - - search_term = command.arg - - # Attempt to get definitions from the Urban Dictionary API. - http_response = requests.get(URBAN_API_ENDPOINT, params={'term': search_term}) - if http_response.status_code != 200: - bot.post_message(command.channel_id, - 'There was an error accessing the Urban Dictionary API.') - return - results = http_response.json() - - # Filter for exact matches - filtered_definitions = filter(lambda def_: def_['word'].casefold() == search_term.casefold(), - results['list']) - - # Sort definitions by their number of thumbs ups. - sorted_definitions = sorted(filtered_definitions, key=lambda def_: def_['thumbs_up'], - reverse=True) - - # If search phrase is not found, notify user. - if len(sorted_definitions) == 0: - bot.post_message(command.channel_id, f'> No results found for {search_term}. ¯\\_(ツ)_/¯') - return - - best_definition = sorted_definitions[0] - # Remove Urban Dictionary [links] - best_definition_text = re.sub(r'[\[\]]', '', best_definition["definition"]) - - # Remove Urban Dictionary [links] - example_text = re.sub(r'[\[\]]', '', best_definition.get('example', '')) - # Break example into individual lines and wrap each in it's own block quote. - example_lines = example_text.split('\r\n') - formatted_example = '\n'.join(f'> {line}' for line in example_lines) - - # Format message and send response to user in channel query was sent from. - message = (f'*{search_term.title()}*\n' - f'{best_definition_text.capitalize()}\n' - f'{formatted_example}') - # Only link back to Urban Dictionary if there are more definitions. - if len(sorted_definitions) > 1: - endpoint_url = http_response.url.replace(URBAN_API_ENDPOINT, URBAN_USER_ENDPOINT) - message += f'\n_ more definitions at {endpoint_url} _' - - bot.post_message(command.channel_id, message) diff --git a/unimplemented/weather.py b/unimplemented/weather.py deleted file mode 100644 index 24425287..00000000 --- a/unimplemented/weather.py +++ /dev/null @@ -1,219 +0,0 @@ -from uqcsbot import bot, Command -from uqcsbot.utils.command_utils import loading_status -from urllib.request import urlopen -import xml.etree.ElementTree as ET -from datetime import datetime as DT -from typing import Union, Tuple - - -def get_xml(state: str) -> Union[None, ET.Element]: - """ - Get BOM data as an XML for a given state - """ - source = {"NSW": "IDN11060", "ACT": "IDN11060", "NT": "IDD10207", "QLD": "IDQ11295", - "SA": "IDS10044", "TAS": "IDT16710", "VIC": "IDV10753", "WA": "IDW14199"} - try: - data = urlopen(f"ftp://ftp.bom.gov.au/anon/gen/fwo/{source[state]}.xml") - root = ET.fromstring(data.read()) - except Exception: - return None - return root - - -def process_arguments(arguments: str) -> Tuple[str, str, int]: - """ - Process the arguments given to !weather, dividing them into state, location and future - Uses default of QLD, Brisbane and 0 if not given - """ - args = arguments.split(" ") if arguments else [] - if args and args[-1].lstrip('-+').isnumeric(): - future = int(args.pop()) - else: - future = 0 - - # get location - if args: - if args[0].upper() in ["NSW", "ACT", "NT", "QLD", "SA", "TAS", "VIC", "WA"]: - state = args.pop(0).upper() - else: - state = "QLD" - location = " ".join(args) - else: - state = "QLD" - location = "Brisbane" - - return state, location, future - - -def find_location(root: ET.Element, location: str, future: int) \ - -> Tuple[Union[None, ET.Element], Union[None, str]]: - """ - Returns the XML for a given the location and how far into the future - """ - node = root.find(f".//area[@description='{location}']") - if node is None: - return None, "Location Not Found" - if node.get("type") != "location": - return None, "Location Given Is Region" - node = node.find(f".//forecast-period[@index='{future}']") - if node is None: - return None, "No Forecast Available For That Day" - return node, None - - -def response_header(node: ET.Element, location: str) -> str: - """ - Returns the response header, in the form "{Location}'s Weather Forecast For {Day}" - """ - forecast_date = DT.strptime("".join(node.get('start-time-local') - .rsplit(":", 1)), "%Y-%m-%dT%H:%M:%S%z").date() - today_date = DT.now().date() - date_delta = (forecast_date - today_date).days - if date_delta == 0: - date_name = "Today" - elif date_delta == 1: - date_name = "Tomorrow" - elif date_delta == -1: - # can happen during the witching hours - date_name = "Yesterday" - else: - date_name = forecast_date.strftime("%A") - return f"*{date_name}'s Weather Forecast For {location}*" - - -def response_overall(node: ET.Element) -> str: - """ - Returns the overall forecast - """ - icon_code = node.find(".//element[@type='forecast_icon_code']") - if icon_code is not None: - icon = ["", "sunny", "clear", "partly-cloudy", "cloudy", "", "haze", "", "light-rain", - "wind", "fog", "showers", "rain", "dust", "frost", "snow", "storm", - "light-showers", "heavy-showers", "tropicalcyclone"][int(icon_code.text)] - icon = f":bom_{icon}:" if icon else "" - descrip = node.find(".//text[@type='precis']") - if descrip is not None: - return f"{icon} {descrip.text} {icon}" - return "" - - -def response_temperature(node: ET.Element) -> str: - """ - Returns the temperature forecast - """ - temp_min = node.find(".//element[@type='air_temperature_minimum']") - temp_max = node.find(".//element[@type='air_temperature_maximum']") - if temp_min is not None and temp_max is not None: - return f"Temperature: {temp_min.text}ºC - {temp_max.text}ºC" - elif temp_min is not None: - return f"Minimum Temperature: {temp_min.text}ºC" - elif temp_max is not None: - return f"Maximum Temperature: {temp_max.text}ºC" - return "" - - -def response_precipitation(node: ET.Element) -> str: - """ - Returns the precipitation forecast - """ - rain_range = node.find(".//element[@type='precipitation_range']") - precip_prob = node.find(".//text[@type='probability_of_precipitation']") - if rain_range is not None and precip_prob is not None: - return f"{precip_prob.text} Chance of Precipitation; {rain_range.text}" - elif precip_prob is not None: - return f"{precip_prob.text} Chance of Precipitation" - return "" - - -def response_brisbane_detailed() -> Tuple[str, str, str]: - """ - Returns a detailed forecast for Brisbane - """ - try: - data = urlopen("ftp://ftp.bom.gov.au/anon/gen/fwo/IDQ10605.xml") - root = ET.fromstring(data.read()) - except Exception: - return "", "", "" - node = root.find(".//area[@description='Brisbane']") - if node is None: - return "", "", "" - node = node.find(".//forecast-period[@index='0']") - if node is None: - return "", "", "" - - forecast_node = node.find(".//text[@type='forecast']") - forecast = "" if forecast_node is None else forecast_node.text - - fire_danger_node = node.find(".//text[@type='fire_danger']") - if fire_danger_node is None or fire_danger_node.text == "Low-Moderate": - fire_danger = "" - else: - fire_danger = f"There Is A {fire_danger_node.text} Fire Danger Today" - - uv_alert_node = node.find(".//text[@type='uv_alert']") - uv_alert = "" if uv_alert_node is None else uv_alert_node.text - - return (forecast, fire_danger, uv_alert) - - -@bot.on_command('weather') -@loading_status -def handle_weather(command: Command) -> None: - """ - `!weather [[state] location] [day]` - Returns the weather forecast for a location - `day` is how many days into the future the forecast is for (0 is today and default) - `location` defaults to Brisbane, and `state` defaults to QLD - """ - - (state, location, future) = process_arguments(command.arg) - - root = get_xml(state) - if root is None: - failure_respone = bot.post_message(command.channel_id, "Could Not Retrieve BOM Data") - bot.api.reactions.add(channel=failure_respone["channel"], - timestamp=failure_respone["ts"], name="disapproval") - return - - node, find_response = find_location(root, location, future) - if node is None: - bot.post_message(command.channel_id, find_response) - return - - # get responses - response = [] - response.append(response_header(node, location)) - response.append(response_overall(node)) - response.append(response_temperature(node)) - response.append(response_precipitation(node)) - # post - bot.post_message(command.channel_id, "\n".join([r for r in response if r])) - - -@bot.on_schedule('cron', hour=6, minute=0, timezone='Australia/Brisbane') -def daily_weather() -> None: - """ - Posts today's Brisbane weather at 6:00am every day - """ - - (state, location, future) = ("QLD", "Brisbane", 0) - - root = get_xml(state) - if root is None: - return - - node, find_response = find_location(root, location, future) - if node is None: - return - - # get responses - response = [] - brisbane_detailed, brisbane_fire, brisbane_uv = response_brisbane_detailed() - response.append(response_header(node, location)) - response.append(response_overall(node)) - response.append(brisbane_detailed) - response.append(response_temperature(node)) - response.append(brisbane_fire) - response.append(brisbane_uv) - # post - general = bot.channels.get("general") - bot.post_message(general.id, "\n".join([r for r in response if r])) diff --git a/unimplemented/welcome.py b/unimplemented/welcome.py deleted file mode 100644 index f8cfdd18..00000000 --- a/unimplemented/welcome.py +++ /dev/null @@ -1,93 +0,0 @@ -""" -Welcomes new users to UQCS Slack and check for member milestones -""" -from uqcsbot import bot -import time - -MEMBER_MILESTONE = 50 # Number of members between posting a celebration -MESSAGE_PAUSE = 2.5 # Number of seconds between sending bot messages -SLACK_DOWNLOAD_GUIDE = "https://slack.com/intl/en-au/help/categories/360000049043-Getting-" \ - "Started#download-the-slack-app" -UQCSBOT_REPO = "https://github.com/UQComputingSociety/uqcsbot" -SLACK_PROFILE_GUIDE = "https://slack.com/intl/en-au/help/articles/204092246-Edit-your-profile" - -WELCOME_MESSAGES = [ # Welcome messages sent to new members - "Hey there! Welcome to the UQCS Slack!", - - "This is the first time I've seen you, so you're probably new here.", - - f"I'm UQCSbot, your friendly (<{UQCSBOT_REPO}|open source>) robot helper!", - - "You can type `help` here, or `!help` anywhere else to find out what I can do!", - - "We've got a bunch of generic channels (e.g. <#C0D0BEYPM|banter>," - " <#C0DKX7NGP|games>, <#CB2K0Q09K|adulting>) along with many subject-specific" - " ones (e.g <#C0MAN4BRS|csse1001>, <#C0Q2KTCK1|math1051>, <#C0DKSDGLE|csse2310>).", - - "To find and join a channel, tap on the channels header in the sidebar.", - - "The UQCS Slack is a friendly community and we have a code of conduct in place to ensure our " - "members' well-being and safety. You can view a copy of this code of conduct here:" - "\n>https://github.com/UQComputingSociety/code-of-conduct", - - "UQCS elects a leadership committee every year who also serve as our friendly Slack admins. " - "This year's committee consists of:\n" - ">James (<@U9D6J8HB8>), Sanni (<@UM55HGLUT>), Kenton (<@U9LMBPJG5>), " - "Darren (<@U4B6LPU2J>), Madhav (<@UFB9R5QFM>), Matthew (<@U8JN3NR1C>), " - "Olivia (<@UA25BSPGT>), Paul (<@UTYTKAB89>), Sylvia (<@U01BXR5TX9T>), " - "Tom (<@UAGPENV96>), and Utkarsh (<@U010W5VDR36>).\n" - "If you have any questions or need to get in touch, please reach out to them.", - - "We also hold heaps of events events during semester. For a list of upcoming events check " - "out the <#C0D0G52PP|events> channel and use the command `!events`.", - - "Don't forget to let people know who you are! Choose a profile pic, set a " - "status and tell us what you're studying. These small touches help the UQCS " - "community understand who you are and, in turn, help you to more quickly " - "become an integral part of the society. If you need any help, check out " - f"this handy <{SLACK_PROFILE_GUIDE}|guide> or ask one admins for help! " - "Once that's all done, why not introduce yourself in <#C2R8W0YPJ|general>.", - - "Be sure to download the Slack and " - f"<{SLACK_DOWNLOAD_GUIDE}|mobile> apps as well, so you'll be able to catch any important " - "announcements, and again, welcome to the UQ Computing Society :)" -] - - -@bot.on("member_joined_channel") -def welcome(evt: dict): - """ - Welcomes new users to UQCS Slack and checks for member milestones. - - @no_help - """ - chan = bot.channels.get(evt.get('channel')) - if chan is None or chan.name != "announcements": - return - - announcements = chan - general = bot.channels.get("general") - user = bot.users.get(evt.get("user")) - - if user is None or user.is_bot: - return - - # Welcome user in general. - bot.post_message(general, f"Welcome to UQCS, <@{user.user_id}>! :tada:") - - # Calculate number of members, ignoring deleted users and bots. - num_members = 0 - for member_id in announcements.members: - member = bot.users.get(member_id) - if any([member is None, member.deleted, member.is_bot]): - continue - num_members += 1 - - # Alert general of any member milestone. - if num_members % MEMBER_MILESTONE == 0: - bot.post_message(general, f":tada: {num_members} members! :tada:") - - # Send new user their welcome messages. - for message in WELCOME_MESSAGES: - time.sleep(MESSAGE_PAUSE) - bot.post_message(user.user_id, message) diff --git a/unimplemented/wiki.py b/unimplemented/wiki.py deleted file mode 100644 index 194fd39b..00000000 --- a/unimplemented/wiki.py +++ /dev/null @@ -1,45 +0,0 @@ -from uqcsbot import bot, Command -import requests -import json -from uqcsbot.utils.command_utils import UsageSyntaxException - - -@bot.on_command("wiki") -def handle_wiki(command: Command): - """ - `!wiki ` - Returns a snippet of text from a relevent wikipedia entry. - """ - if not command.has_arg(): - raise UsageSyntaxException() - - search_query = command.arg - api_url = f"https://en.wikipedia.org/w/api.php?action=opensearch&format=json&limit=2" - - http_response = requests.get(api_url, params={'search': search_query}) - if http_response.status_code != requests.codes.ok: - bot.post_message(command.channel_id, "Problem fetching data") - return - - _, title_list, snippet_list, url_list = json.loads(http_response.content) - - # If the results are empty let them know. Any list being empty signifies this. - if len(title_list) == 0: - bot.post_message(command.channel_id, "No Results Found.") - return - - title, snippet, url = title_list[0], snippet_list[0], url_list[0] - - # Sometimes the first element is an empty string which is weird so we handle that rare case here - if len(title) * len(snippet) * len(url) == 0: - bot.post_message(command.channel_id, "Sorry, there was something funny about the result") - return - - # Detect if there are multiple references to the query - # if so, use the first reference (i.e. the second item in the lists). - multiple_reference_instances = ("may refer to:", "may have several meanings:") - if any(instance in snippet for instance in multiple_reference_instances): - title, snippet, url = title_list[1], snippet_list[1], url_list[1] - - # The first url and title matches the first snippet containing any content - message = f'{title}: {snippet}\nLink: {url}' - bot.post_message(command.channel_id, message) diff --git a/unimplemented/wolfram.py b/unimplemented/wolfram.py deleted file mode 100644 index c9b6bbcb..00000000 --- a/unimplemented/wolfram.py +++ /dev/null @@ -1,251 +0,0 @@ -from uqcsbot import bot, Command -from typing import Iterable, Tuple, Optional -import requests -import json -import os -from uqcsbot.utils.command_utils import loading_status, UsageSyntaxException - -WOLFRAM_APP_ID = os.environ.get('WOLFRAM_APP_ID') - - -def get_subpods(pods: list) -> Iterable[Tuple[str, dict]]: - """ - Yields subpods in the order they should be displayed. A subpod is essentially an element - of a wolfram response. For example one pod might be "Visual Representation" and the - subpod is a graph of your input. Every pod has at least one subpod (usually only one). - - Yield: (pod_or_subpod_title, subpod) - """ - for pod in pods: - for subpod in pod["subpods"]: - # Use the pods title if the subpod doesn't have its own title (general case) - title = pod['title'] if len(subpod['title']) == 0 else subpod['title'] - yield (title, subpod) - - -@bot.on_command('wolfram') -@loading_status -def handle_wolfram(command: Command): - """ - `!wolfram [--full] ` - Returns the wolfram response for the - given query. If `--full` is specified, will return the full reponse. - """ - if not command.has_arg(): - raise UsageSyntaxException() - - # Determines whether to use the full version or the short version. The full - # version is used if the --full. argument is supplied before or after the - # search query. See wolfram_full and wolfram_normal for the differences. - cmd = command.arg.strip() - # Doing it specific to the start and end just in case someone - # has --full inside their query for whatever reason. - if cmd.startswith('--full'): - cmd = cmd[len('--full'):] # removes the --full - wolfram_full(cmd, command.channel_id) - elif cmd.endswith('--full'): - cmd = cmd[:-len('--full')] # removes the --full - wolfram_full(cmd, command.channel_id) - else: - wolfram_normal(cmd, command.channel_id) - - -def wolfram_full(search_query: str, channel): - """ - This posts the full results from wolfram query. Images and all - - Example usage: - !wolfram --full y = 2x + c - """ - api_url = "http://api.wolframalpha.com/v2/query?&output=json" - http_response = requests.get(api_url, params={'input': search_query, 'appid': WOLFRAM_APP_ID}) - - # Check if the response is ok - if http_response.status_code != requests.codes.ok: - bot.post_message(channel, "There was a problem getting the response") - return - - # Get the result of the query and determine if wolfram succeeded in evaluating it - result = json.loads(http_response.content)['queryresult'] - if not result['success'] or result["error"]: - bot.post_message(channel, "Please rephrase your query. Wolfram could not compute.") - return - - # A pod is the name wolfram gives to the different "units" that make up its result. - # For example a pod may be a "Visual Representation" of the input. - # Essentially they are logical components. Each pod has one or more subpods that compose it. - message = "" - for title, subpod in get_subpods(result['pods']): - plaintext = subpod["plaintext"] - - # Prefer a plain text representation to the image - if plaintext != "" and plaintext != "* * * * * *": - message += f'{title}: {plaintext}\n' - else: - image_url = subpod['img']['src'] - image_title = subpod['img']['title'] - if len(image_title) > 0: - message += f'{image_title}:\n{image_url}\n' - else: - message += f'{image_url}\n' - bot.post_message(channel, message) - - -def get_short_answer(search_query: str): - """ - This uses wolfram's short answers api to just return a simple short plaintext response. - - This is used if the conversation api fails to get a result (for instance !wolfram - pineapple is not a great conversation starter but may be interesting). - """ - api_url = "http://api.wolframalpha.com/v2/result?" - http_response = requests.get(api_url, params={'input': search_query, 'appid': WOLFRAM_APP_ID}) - - # Check if the response is ok. A status code of 501 signifies that no result could be found. - if http_response.status_code == 501: - return "No short answer available. Try !wolfram --full" - elif http_response.status_code != requests.codes.ok: - return "There was a problem getting the response" - - return http_response.content - - -def wolfram_normal(search_query: str, channel): - """ - This uses wolfram's conversation api to return a short response - that can be replied to in a thread. If the response cannot be - replied to a general short answer response is displayed instead. - - Example Usage: - !wolfram Solve Newton's Second Law for mass - !wolfram What is the distance from Earth to Mars? - - and then start a thread to continue the conversation - """ - result, conversation_id, reply_host, s_output = conversation_request(search_query) - - if conversation_id is None: - if result == "No result is available": - # If no conversational result is available just return a normal short answer - short_response = get_short_answer(search_query) - bot.post_message(channel, short_response) - return - else: - bot.post_message(channel, result) - return - - # TODO(mubiquity): Is there a better option than storing the id in the fallback? - # Here we store the conversation ID in the fallback so we can get it back later. - # We also store an identifier string to check against later and the reply_host and s_output - # string. Attachments is a slack thing that allows the formatting or more complex messages. - # In this case we add a footer and use the fallback to cheekily store information for later. - attachments = [{'fallback': f'WolframCanReply {reply_host} {s_output} {conversation_id}', - 'footer': 'Further questions may be asked', - 'text': result}] - - bot.post_message(channel, "", attachments=attachments) - - -def extract_reply(wolfram_response: dict) -> Tuple[str, str, str, str]: - """ - Takes the response from the conversations API and returns it as a tuple containing - the reply, conversation id, reply host and s parameters. In that order. - """ - - return (wolfram_response['result'], # This is the answer to our question - wolfram_response['conversationID'], # Used to continue the conversation - wolfram_response['host'], # This is the hostname to ask the next question to - wolfram_response.get('s')) # s is only sometimes returned, but is vital - - -def conversation_request( - search_query: str, - host_name: Optional[str] = None, - conversation_id: Optional[str] = None, - s_output: Optional[str] = None -): - """ - Makes a request for either the first stage of the conversation (don't supply a - conversation_id and s_output) or for a continued stage of the conversation (do supply them). - It will return four values. In the case of an error it will return an error string that - can be posted to the user and 3 Nones or it will return the result of the question, - the new conversation_id, the new host name and the new s_output. In that order. - """ - # The format of the api urls is slightly different if a conversation is being continued - # (has a conversation_id). Any of the following would suffice but may as well be thorough - if any([host_name is None, conversation_id is None, s_output is None]): - api_url = "http://api.wolframalpha.com/v1/conversation.jsp?" - params = {'appid': WOLFRAM_APP_ID, 'i': search_query} - else: - # Slack annoyingly formats the reply_host link so we have to extract what we want: - # The format is - host_name = host_name[1:-1].split('|')[0] - api_url = f'{host_name}/api/v1/conversation.jsp?' - params = {'appid': WOLFRAM_APP_ID, 'i': search_query, - 'conversationid': conversation_id, 's': s_output} - - http_response = requests.get(api_url, params=params) - - if http_response.status_code != requests.codes.ok: - return "There was a problem getting the response", None, None, None - - # Convert to json and check for an error - wolfram_answer = json.loads(http_response.content) - if 'error' in wolfram_answer: - return wolfram_answer['error'], None, None, None - - return extract_reply(wolfram_answer) - - -@bot.on('message') -def handle_reply(evt: dict): - """ - Handles a message event. Whenever a message is a reply to one of !wolframs conversational - results this handles getting the next response and updating the old stored information. - """ - # If the message isn't from a thread or is from a bot ignore it (avoid those infinite loops) - if 'thread_ts' not in evt or evt.get('subtype') == 'bot_message': - return - - channel = evt['channel'] - thread_ts = evt['thread_ts'] # This refers to time the original message - thread_parent = bot.api.conversations.history(channel=channel, limit=1, - inclusive=True, latest=thread_ts) - - if not thread_parent['ok']: - # The most likely reason for this error is auth issues or possibly rate limiting - bot.logger.error(f'Error with wolfram script thread history: {thread_parent}') - return - - # Limit=1 was used so the first (and only) message is what we want - parent_message = thread_parent['messages'][0] - # If the threads parent wasn't by a bot ignore it - if parent_message.get('subtype') != 'bot_message': - return - - # Finally, we have to check that this is a Wolfram replyable message - # It is rare we would reach this point and not pass as who - # replies to a bot in a thread for another reason? - parent_attachment = parent_message['attachments'][0] # Only one attachment to get - parent_fallback = parent_attachment['fallback'] - if 'WolframCanReply' not in parent_fallback: - return - - # Now we can grab the conversation_id from the message - # and get the new question (s only sometimes appears). - # Recall the format of the fallback "identifier hostname s_output conversationID" - _, reply_host, s_output, conversation_id = parent_fallback.split(' ') - new_question = evt['text'] # This is the value of the message that triggered the response - s_output = '' if s_output is None else s_output - - # Ask Wolfram for the new answer grab the new stuff and post the reply. - reply, conversation_id, reply_host, s_output = conversation_request(new_question, reply_host, - conversation_id, s_output) - - bot.post_message(channel, reply, thread_ts=thread_ts) - - # If getting a the conversation request results in an error then conversation_id will be None - if conversation_id is not None: - # Update the old fallback to reflect the new state of the conversation - parent_attachment['fallback'] = f'WolframCanReply {reply_host} {s_output} {conversation_id}' - - bot.api.chat.update(channel=channel, attachments=[parent_attachment], ts=thread_ts) diff --git a/unimplemented/yt.py b/unimplemented/yt.py deleted file mode 100644 index a29920be..00000000 --- a/unimplemented/yt.py +++ /dev/null @@ -1,63 +0,0 @@ -import os -from uqcsbot import bot, Command -from uqcsbot.utils.command_utils import UsageSyntaxException -from googleapiclient.discovery import build -from googleapiclient.errors import HttpError - -YOUTUBE_API_KEY = os.environ.get('YOUTUBE_API_KEY') -YOUTUBE_API_SERVICE_NAME = 'youtube' -YOUTUBE_API_VERSION = 'v3' -YOUTUBE_VIDEO_URL = 'https://www.youtube.com/watch?v=' -NO_QUERY_MESSAGE = "You can't look for nothing. !yt " - - -@bot.on_command('yt') -def handle_yt(command: Command): - """ - `!yt ` - Returns the top video search result based on the query string. - """ - # Makes sure the query is not empty. - if not command.has_arg(): - raise UsageSyntaxException() - - search_query = command.arg.strip() - try: - videoID = get_top_video_result(search_query, command.channel_id) - except HttpError as e: - # Googleapiclient should handle http errors - bot.logger.error( - f'An HTTP error {e.resp.status} occurred:\n{e.content}') - # Force return to ensure no message is sent. - return - - if videoID: - bot.post_message(command.channel_id, f'{YOUTUBE_VIDEO_URL}{videoID}') - else: - bot.post_message(command.channel_id, "Your query returned no results.") - - -def get_top_video_result(search_query: str, channel): - """ - The normal method for using !yt searches based on query - and returns the first video result. "I'm feeling lucky" - """ - search_response = execute_search(search_query, 'id', 'video', 1) - search_result = search_response.get('items') - if search_result is None: - return None - return search_result[0]['id']['videoId'] - - -def execute_search(search_query: str, search_part: str, search_type: str, max_results: int): - """ - Executes the search via the google api client based on the parameters given. - """ - youtube = build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION, - developerKey=YOUTUBE_API_KEY, cache_discovery=False) - - search_response = youtube.search().list(q=search_query, - part=search_part, - maxResults=max_results, - type=search_type).execute() - - return search_response