From c61622528754fd383e618716f3a5247111fb5555 Mon Sep 17 00:00:00 2001 From: James Dearlove <39483549+JamesDearlove@users.noreply.github.com> Date: Tue, 17 Oct 2023 21:03:00 +1000 Subject: [PATCH 1/2] Added logging for holidays cog (#169) * Added logging for holidays cog * Extra log was not necessary * Caught out by the style guide once more --- uqcsbot/holidays.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/uqcsbot/holidays.py b/uqcsbot/holidays.py index d4eaea07..e8a2f957 100644 --- a/uqcsbot/holidays.py +++ b/uqcsbot/holidays.py @@ -113,8 +113,11 @@ async def holiday(self): Posts a random celebratory day on #general from https://www.timeanddate.com/holidays/fun/ """ + logging.info("Running daily holiday task") + holiday = get_holiday() if holiday is None: + logging.info("No holiday was found for today") return general_channel = discord.utils.get( From 05c1e052d0ed9ed0cfed2e522b255407bfc4c01d Mon Sep 17 00:00:00 2001 From: bradleysigma <42644678+bradleysigma@users.noreply.github.com> Date: Sat, 28 Oct 2023 09:28:08 +1000 Subject: [PATCH 2/2] Remove Ununimplemented (#168) * Remove Ununimplemented * Delete cookbook.py --------- Co-authored-by: Andrew Brown <92134285+andrewj-brown@users.noreply.github.com> --- unimplemented/calendar.py | 74 --------------- unimplemented/channel_log.py | 13 --- unimplemented/coin.py | 20 ---- unimplemented/cookbook.py | 10 -- unimplemented/dominos.py | 116 ----------------------- unimplemented/emoji_log.py | 37 -------- unimplemented/emojify.py | 173 ----------------------------------- unimplemented/history.py | 56 ------------ unimplemented/id.py | 9 -- unimplemented/link.py | 163 --------------------------------- unimplemented/pastexams.py | 67 -------------- unimplemented/wavie.py | 22 ----- unimplemented/whoami.py | 18 ---- unimplemented/xkcd.py | 125 ------------------------- 14 files changed, 903 deletions(-) delete mode 100644 unimplemented/calendar.py delete mode 100644 unimplemented/channel_log.py delete mode 100644 unimplemented/coin.py delete mode 100644 unimplemented/cookbook.py delete mode 100644 unimplemented/dominos.py delete mode 100644 unimplemented/emoji_log.py delete mode 100644 unimplemented/emojify.py delete mode 100644 unimplemented/history.py delete mode 100644 unimplemented/id.py delete mode 100644 unimplemented/link.py delete mode 100644 unimplemented/pastexams.py delete mode 100644 unimplemented/wavie.py delete mode 100644 unimplemented/whoami.py delete mode 100644 unimplemented/xkcd.py diff --git a/unimplemented/calendar.py b/unimplemented/calendar.py deleted file mode 100644 index a14806b0..00000000 --- a/unimplemented/calendar.py +++ /dev/null @@ -1,74 +0,0 @@ -from datetime import datetime -from icalendar import Calendar, Event -from uuid import uuid4 as uuid -from uqcsbot import bot, Command -from uqcsbot.utils.command_utils import loading_status, success_status -from uqcsbot.utils.uq_course_utils import (get_course_assessment, - get_parsed_assessment_due_date, - HttpException, - CourseNotFoundException, - ProfileNotFoundException, - DateSyntaxException) - -# Maximum number of courses supported by !calendar to reduce call abuse. -COURSE_LIMIT = 6 - - -def get_calendar(assessment): - """ - Returns a compiled calendar containing the given assessment. - """ - calendar = Calendar() - for assessment_item in assessment: - course, task, due_date, weight = assessment_item - event = Event() - event['uid'] = str(uuid()) - event['summary'] = f'{course} ({weight}): {task}' - try: - start_datetime, end_datetime = get_parsed_assessment_due_date(assessment_item) - except DateSyntaxException as e: - bot.logger.error(e.message) - # If we can't parse a date, set its due date to today - # and let the user know through its summary. - # TODO(mitch): Keep track of these instances to attempt to accurately - # parse them in future. Will require manual detection + parsing. - start_datetime = end_datetime = datetime.today() - event['summary'] = ("WARNING: DATE PARSING FAILED\n" - "Please manually set date for event!\n" - "The provided due date from UQ was" - + f" '{due_date}\'. {event['summary']}") - event.add('dtstart', start_datetime) - event.add('dtend', end_datetime) - calendar.add_component(event) - return calendar.to_ical() - - -@bot.on_command('calendar') -@success_status -@loading_status -def handle_calendar(command: Command): - """ - `!calendar [COURSE CODE 2] ...` - Returns a compiled - calendar containing all the assessment for a given list of course codes. - """ - channel = bot.channels.get(command.channel_id) - course_names = command.arg.split() if command.has_arg() else [channel.name] - - if len(course_names) > COURSE_LIMIT: - bot.post_message(channel, f'Cannot process more than {COURSE_LIMIT} courses.') - return - - try: - assessment = get_course_assessment(course_names) - 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 - - user_direct_channel = bot.channels.get(command.user_id) - bot.api.files.upload(title='Importable calendar containing your assessment!', - channels=user_direct_channel.id, filetype='text/calendar', - filename='assessment.ics', file=get_calendar(assessment)) diff --git a/unimplemented/channel_log.py b/unimplemented/channel_log.py deleted file mode 100644 index d1c5c175..00000000 --- a/unimplemented/channel_log.py +++ /dev/null @@ -1,13 +0,0 @@ -from uqcsbot import bot - - -@bot.on("channel_created") -def channel_log(evt: dict): - """ - Notes when channels are created in #uqcs-meta - - @no_help - """ - bot.post_message(bot.channels.get("uqcs-meta"), - 'New Channel Created: ' - + f'<#{evt.get("channel").get("id")}|{evt.get("channel").get("name")}>') diff --git a/unimplemented/coin.py b/unimplemented/coin.py deleted file mode 100644 index ca5d2ae4..00000000 --- a/unimplemented/coin.py +++ /dev/null @@ -1,20 +0,0 @@ -from random import choice -from uqcsbot import bot, Command - - -@bot.on_command("coin") -def handle_coin(command: Command): - """ - `!coin [number]` - Flips 1 or more coins. - """ - if command.has_arg() and command.arg.isnumeric(): - flips = min(max(int(command.arg), 1), 500) - else: - flips = 1 - - response = [] - emoji = (':heads:', ':tails:') - for i in range(flips): - response.append(choice(emoji)) - - bot.post_message(command.channel_id, "".join(response)) diff --git a/unimplemented/cookbook.py b/unimplemented/cookbook.py deleted file mode 100644 index 34d5948a..00000000 --- a/unimplemented/cookbook.py +++ /dev/null @@ -1,10 +0,0 @@ -from uqcsbot import bot, Command - - -@bot.on_command("cookbook") -def handle_cookbook(command: Command): - """ - `!cookbook` - Returns the URL of the UQCS student-compiled cookbook (pdf). - """ - bot.post_message(command.channel_id, "It's A Cookbook!\n" - "https://github.com/UQComputingSociety/cookbook") diff --git a/unimplemented/dominos.py b/unimplemented/dominos.py deleted file mode 100644 index a75318be..00000000 --- a/unimplemented/dominos.py +++ /dev/null @@ -1,116 +0,0 @@ -import argparse -from uqcsbot import bot, Command -from bs4 import BeautifulSoup -from datetime import datetime -from requests.exceptions import RequestException -from typing import List -from uqcsbot.utils.command_utils import loading_status, UsageSyntaxException -import requests - -MAX_COUPONS = 10 # Prevents abuse -COUPONESE_DOMINOS_URL = 'https://www.couponese.com/store/dominos.com.au/' - - -class Coupon: - def __init__(self, code: str, expiry_date: str, description: str) -> None: - self.code = code - self.expiry_date = expiry_date - self.description = description - - def is_valid(self) -> bool: - try: - expiry_date = datetime.strptime(self.expiry_date, '%Y-%m-%d') - now = datetime.now() - return all([expiry_date.year >= now.year, expiry_date.month >= now.month, - expiry_date.day >= now.day]) - except ValueError: - return True - - def keyword_matches(self, keyword: str) -> bool: - return keyword.lower() in self.description.lower() - - -@bot.on_command("dominos") -@loading_status -def handle_dominos(command: Command): - """ - `!dominos [--num] N [--expiry] ` - Returns a list of dominos coupons (default: 5 | max: 10) - """ - command_args = command.arg.split() if command.has_arg() else [] - - parser = argparse.ArgumentParser() - - def usage_error(*args, **kwargs): - raise UsageSyntaxException() - parser.error = usage_error # type: ignore - parser.add_argument('-n', '--num', default=5, type=int) - parser.add_argument('-e', '--expiry', action='store_true') - parser.add_argument('keywords', nargs='*') - - args = parser.parse_args(command_args) - coupons_amount = min(args.num, MAX_COUPONS) - coupons = get_coupons(coupons_amount, args.expiry, args.keywords) - - message = "" - for coupon in coupons: - message += f"Code: *{coupon.code}* - {coupon.description}\n" - bot.post_message(command.channel_id, message) - - -def filter_coupons(coupons: List[Coupon], keywords: List[str]) -> List[Coupon]: - """ - Filters coupons iff a keyword is found in the description. - """ - return [coupon for coupon in coupons if - any(coupon.keyword_matches(keyword) for keyword in keywords)] - - -def get_coupons(n: int, ignore_expiry: bool, keywords: List[str]) -> List[Coupon]: - """ - Returns a list of n Coupons - """ - - coupon_page = get_coupon_page() - if coupon_page is None: - return None - - coupons = get_coupons_from_page(coupon_page) - - if not ignore_expiry: - coupons = [coupon for coupon in coupons if coupon.is_valid()] - - if keywords: - coupons = filter_coupons(coupons, keywords) - return coupons[:n] - - -def get_coupons_from_page(coupon_page: bytes) -> List[Coupon]: - """ - Strips results from html page and returns a list of Coupon(s) - """ - soup = BeautifulSoup(coupon_page, 'html.parser') - soup_coupons = soup.find_all(class_="ov-coupon") - - coupons = [] - - for soup_coupon in soup_coupons: - expiry_date_str = soup_coupon.find(class_='ov-expiry').get_text(strip=True) - description = soup_coupon.find(class_='ov-desc').get_text(strip=True) - code = soup_coupon.find(class_='ov-code').get_text(strip=True) - coupon = Coupon(code, expiry_date_str, description) - coupons.append(coupon) - - return coupons - - -def get_coupon_page() -> bytes: - """ - Gets the coupon page HTML - """ - try: - response = requests.get(COUPONESE_DOMINOS_URL) - return response.content - except RequestException as e: - bot.logger.error(e.response.content) - return None diff --git a/unimplemented/emoji_log.py b/unimplemented/emoji_log.py deleted file mode 100644 index aedb8d12..00000000 --- a/unimplemented/emoji_log.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -Logs emoji addition/removal to emoji-request for audit purposes -""" -from uqcsbot import bot - - -@bot.on("emoji_changed") -def emoji_log(evt: dict): - """ - Notes when emojis are added or deleted. - - @no_help - """ - emoji_request = bot.channels.get("emoji-request") - subtype = evt.get("subtype") - - if subtype == 'add': - name = evt["name"] - value = evt["value"] - - if value.startswith('alias:'): - _, alias = value.split('alias:') - - bot.post_message(emoji_request, - f'Emoji alias added: `:{name}:` :arrow_right: `:{alias}:` (:{name}:)') - - else: - message = bot.post_message(emoji_request, f'Emoji added: :{name}: (`:{name}:`)') - bot.api.reactions.add(channel=message["channel"], - timestamp=message["ts"], name=name) - - elif subtype == 'remove': - names = evt.get("names") - removed = ', '.join(f'`:{name}:`' for name in names) - plural = 's' if len(names) > 1 else '' - - bot.post_message(emoji_request, f'Emoji{plural} removed: {removed}') diff --git a/unimplemented/emojify.py b/unimplemented/emojify.py deleted file mode 100644 index af131ec3..00000000 --- a/unimplemented/emojify.py +++ /dev/null @@ -1,173 +0,0 @@ -from uqcsbot import bot, Command -from uqcsbot.utils.command_utils import loading_status -from typing import Dict, List - -from collections import defaultdict -from random import shuffle, choice - - -@bot.on_command("emojify") -@loading_status -def handle_emojify(command: Command): - ''' - `!emojify text` - converts text to emoji. - ''' - master: Dict[str, List[str]] = defaultdict(lambda: [":grey_question:"]) - - # letters - master['A'] = [":adobe:", ":airbnb:", ":amazon:", ":anarchism:", - ":arch:", ":atlassian:", ":office_access:", ":capital_a_agile:", - choice([":card-ace-clubs:", ":card-ace-diamonds:", - ":card-ace-hearts:", ":card-ace-spades:"])] - master['B'] = [":bhinking:", ":bitcoin:", ":blutes:"] - master['C'] = [":c:", ":clang:", ":cplusplus:", ":copyright:", ":clipchamp:", - ":clipchamp_old:"] - master['D'] = [":d:", ":disney:", ":deloitte:"] - master['E'] = [":ecorp:", ":emacs:", ":erlang:", ":ie10:", ":thonk_slow:", ":edge:", - ":expedia_group:"] - master['F'] = [":f:", ":facebook:", ":flutter:", ":figma:"] - master['G'] = [":g+:", ":google:", ":nintendo_gamecube:", ":gatsbyjs:", ":gmod:"] - master['H'] = [":hackerrank:", ":homejoy:"] - master['I'] = [":information_source:", ":indoorooshs:"] - master['J'] = [choice([":card-jack-clubs:", ":card-jack-diamonds:", - ":card-jack-hearts:", ":card-jack-spades:"])] - master['K'] = [":kickstarter:", ":kotlin:", - choice([":card-king-clubs:", ":card-king-diamonds:", - ":card-king-hearts:", ":card-king-spades:"])] - master['L'] = [":l:", ":lime:", ":l_plate:", ":ti_nekro:"] - master['M'] = [":gmail:", ":maccas:", ":mcgrathnicol:", ":melange_mining:", ":mtg:", ":mxnet:", - ":jmod:"] - master['N'] = [":nano:", ":neovim:", ":netscape_navigator:", ":notion:", - ":nginx:", ":nintendo_64:", ":office_onenote:", ":netflix-n:"] - master['O'] = [":office_outlook:", ":oracle:", ":o_:", ":tetris_o:", ":ubuntu:", - choice([":portal_blue:", ":portal_orange:"])] - master['P'] = [":auspost:", ":office_powerpoint:", ":office_publisher:", - ":pinterest:", ":paypal:", ":producthunt:", ":uqpain:"] - master['Q'] = [":quora:", ":quantium:", choice([":card-queen-clubs:", ":card-queen-diamonds:", - ":card-queen-hearts:", ":card-queen-spades:"])] - master['R'] = [":r-project:", ":rust:", ":redroom:", ":registered:"] - master['S'] = [":s:", ":skedulo:", ":stanford:", ":stripe_s:", ":sublime:", ":tetris_s:"] - master['T'] = [":tanda:", choice([":telstra:", ":telstra-pink:"]), - ":tesla:", ":tetris_t:", ":torchwood:", ":tumblr:", ":nyt:"] - master['U'] = [":uber:", ":uqu:", ":the_horns:", ":proctoru:", ":ubiquiti:"] - master['V'] = [":vim:", ":vue:", ":vuetify:", ":v:"] - master['W'] = [":office_word:", ":washio:", ":wesfarmers:", ":westpac:", - ":weyland_consortium:", ":wikipedia_w:", ":woolworths:"] - master['X'] = [":atlassian_old:", ":aginicx:", ":sonarr:", ":x-files:", ":xbox:", - ":x:", ":flag-scotland:", ":office_excel:"] - master['Y'] = [":hackernews:"] - master['Z'] = [":tetris_z:"] - - # numbers - master['0'] = [":chrome:", ":suncorp:", ":disney_zero:", ":firefox:", - ":mars:", ":0_:", choice([":dvd:", ":cd:"])] - master['1'] = [":techone:", ":testtube:", ":thonk_ping:", ":first_place_medal:", - ":critical_fail:", ":slack_unread_1:"] - master['2'] = [":second_place_medal:", choice([":card-2-clubs:", ":card-2-diamonds:", - ":card-2-hearts:", ":card-2-spades:"])] - master['3'] = [":css:", ":third_place_medal:", choice([":card-3-clubs:", ":card-3-diamonds:", - ":card-3-hearts:", ":card-3-spades:"])] - master['4'] = [choice([":card-4-clubs:", ":card-4-diamonds:", - ":card-4-hearts:"]), ":card-4-spades:"] - master['5'] = [":html:", choice([":card-5-clubs:", ":card-5-diamonds:", - ":card-5-hearts:", ":card-5-spades:"])] - master['6'] = [choice([":card-6-clubs:", ":card-6-diamonds:", - ":card-6-hearts:", ":card-6-spades:"])] - master['7'] = [choice([":card-7-clubs:", ":card-7-diamonds:", - ":card-7-hearts:", ":card-7-spades:"])] - master['8'] = [":8ball:", choice([":card-8-clubs:", ":card-8-diamonds:", - ":card-8-hearts:", ":card-8-spades:"])] - master['9'] = [choice([":card-9-clubs:", ":card-9-diamonds:", - ":card-9-hearts:", ":card-9-spades:"])] - - # whitespace - master[' '] = [":whitespace:"] - master['\n'] = ["\n"] - - # other ascii characters (sorted by ascii value) - master['!'] = [":exclamation:"] - master['"'] = [choice([":ldquo:", ":rdquo:"]), ":pig_nose:"] - master['\''] = [":apostrophe:"] - master['#'] = [":slack_old:", ":csharp:"] - master['$'] = [":thonk_money:", ":moneybag:"] - # '&' converts to '&' - master['&'] = [":ampersand:", ":dnd:"] - master['('] = [":lparen:"] - master[')'] = [":rparen:"] - master['*'] = [":day:", ":nab:", ":youtried:", ":msn_star:", ":rune_prayer:", ":wolfram:", - ":shuriken:", ":mtg_s:", ":aoc:", ":jetstar:"] - master['+'] = [":tf2_medic:", ":flag-ch:", ":flag-england:"] - master['-'] = [":no_entry:"] - master['.'] = [":full_stop_big:"] - master[','] = [":comma:"] - master['/'] = [":slash:"] - master[';'] = [":semi-colon:"] - # '>' converts to '>' - master['>'] = [":accenture:", ":implying:", ":plex:", ":powershell:"] - master['?'] = [":question:"] - master['@'] = [":whip:"] - master['^'] = [":this:", ":typographical_carrot:", ":arrow_up:", - ":this_but_it's_an_actual_caret:"] - master['~'] = [":wavy_dash:"] - - # slack/uqcsbot convert the following to other symbols - - # greek letters - # 'Α' converts to 'A' - master['Α'] = [":alpha:"] - # 'Β' converts to 'B' - master['Β'] = [":beta:"] - # 'Δ' converts to 'D' - master['Δ'] = [":optiver:"] - # 'Λ' converts to 'L' - master['Λ'] = [":halflife:", ":haskell:", ":lambda:", ":racket:", - choice([":uqcs:", ":scrollinguqcs:", ":scrollinguqcs_alt:", ":uqcs_mono:"])] - # 'Π' converts to 'P' - master['Π'] = [":pi:"] - # 'Φ' converts to 'PH' - master['Φ'] = [":phyrexia_blue:"] - # 'Σ' converts to 'S' - master['Σ'] = [":polymathian:", ":sigma:"] - - # other symbols (sorted by unicode value) - # '…' converts to '...' - master['…'] = [":lastpass:"] - # '€' converts to 'EUR' - master['€'] = [":martian_euro:"] - # '√' converts to '[?]' - master['√'] = [":sqrt:"] - # '∞' converts to '[?]' - master['∞'] = [":arduino:", ":visualstudio:", ":infinitely:"] - # '∴' converts to '[?]' - master['∴'] = [":julia:"] - - master['人'] = [":人:"] - - master[chr(127)] = [":delet_this:"] - - text = "" - if command.has_arg(): - text = command.arg.upper() - # revert HTML conversions - text = text.replace(">", ">") - text = text.replace("<", "<") - text = text.replace("&", "&") - - lexicon = {} - for character in set(text+'…'): - full, part = divmod((text+'…').count(character), len(master[character])) - shuffle(master[character]) - lexicon[character] = full * master[character] + master[character][:part] - shuffle(lexicon[character]) - - ellipsis = lexicon['…'].pop() - - response = "" - for character in text: - emoji = lexicon[character].pop() - if len(response + emoji + ellipsis) > 4000: - response += ellipsis - break - response += emoji - - bot.post_message(command.channel_id, response) diff --git a/unimplemented/history.py b/unimplemented/history.py deleted file mode 100644 index 2dd62122..00000000 --- a/unimplemented/history.py +++ /dev/null @@ -1,56 +0,0 @@ -from uqcsbot import bot -from datetime import datetime -from pytz import timezone, utc -from random import choice - - -class Pin: - """ - Class for pins, with channel, age in years, user and pin text - """ - def __init__(self, channel: str, years: int, user: str, text: str): - self.channel = channel - self.years = years - self.user = user - self.text = text - - def message(self) -> str: - return (f"On this day, {self.years} years ago, <@{self.user}> said" - f"\n>>>{self.text}") - - def origin(self): - return bot.channels.get(self.channel) - - -@bot.on_schedule('cron', hour=12, minute=0, timezone='Australia/Brisbane') -def daily_history() -> None: - """ - Selets a random pin that was posted on this date some years ago, - and reposts it in the same channel - """ - anniversary = [] - today = datetime.now(utc).astimezone(timezone('Australia/Brisbane')).date() - - # for every channel - for channel in bot.api.conversations.list(types="public_channel")['channels']: - # skip archived channels - if channel['is_archived']: - continue - - for pin in bot.api.pins.list(channel=channel['id'])['items']: - # messily get the date the pin was originally posted - pin_date = (datetime.fromtimestamp(int(float(pin['message']['ts'])), tz=utc) - .astimezone(timezone('Australia/Brisbane')).date()) - # if same date as today - if pin_date.month == today.month and pin_date.day == today.day: - # add pin to possibilities - anniversary.append(Pin(channel=channel['name'], years=today.year-pin_date.year, - user=pin['message']['user'], text=pin['message']['text'])) - - # if no pins were posted on this date, do nothing - if not anniversary: - return - - # randomly select a pin, and post it in the original channel - selected = choice(anniversary) - bot.post_message(selected.origin(), selected.message()) diff --git a/unimplemented/id.py b/unimplemented/id.py deleted file mode 100644 index 8d7ff00a..00000000 --- a/unimplemented/id.py +++ /dev/null @@ -1,9 +0,0 @@ -from uqcsbot import bot, Command - - -@bot.on_command("id") -def handle_id(command: Command): - """ - `!id` - Returns the calling user's Slack ID. - """ - bot.post_message(command.channel_id, f'You are Number `{command.user_id}`') diff --git a/unimplemented/link.py b/unimplemented/link.py deleted file mode 100644 index 83674dc0..00000000 --- a/unimplemented/link.py +++ /dev/null @@ -1,163 +0,0 @@ -from argparse import ArgumentParser -from enum import Enum -from typing import Optional, Tuple - -from slackblocks import Attachment, Color, SectionBlock -from sqlalchemy.exc import NoResultFound - -from uqcsbot import bot, Command -from uqcsbot.models import Link -from uqcsbot.utils.command_utils import loading_status - - -class LinkScope(Enum): - """ - Possible requested scopes for setting or retrieving a link. - """ - CHANNEL = "channel" - GLOBAL = "global" - - -class SetResult(Enum): - """ - Possible outcomes of the set link operation. - """ - NEEDS_OVERRIDE = "Link already exists, use `-f` to override" - OVERRIDE_SUCCESS = "Successfully overrode link" - NEW_LINK_SUCCESS = "Successfully added link" - - -def set_link_value(key: str, value: str, channel: str, - override: bool, link_scope: Optional[LinkScope] = None) -> Tuple[SetResult, str]: - """ - Sets a corresponding value for a particular key. Keys are set to global by default but this can - be overridden by passing the channel flag. Existing links can only be overridden if the - override flag is passed. - :param key: the lookup key for users to search the value by - :param value: the value to associate with the key - :param channel: the name of the channel the set operation was initiated in - :param link_scope: defines the scope to set the link in, defaults to global if not provided - :param override: required to be True if an association already exists and needs to be updated - :return: a SetResult status and the value associated with the given key/channel combination - """ - link_channel = channel if link_scope == LinkScope.CHANNEL else None - session = bot.create_db_session() - - try: - exists = session.query(Link).filter(Link.key == key, - Link.channel == link_channel).one() - if exists and not override: - return SetResult.NEEDS_OVERRIDE, exists.value - session.delete(exists) - result = SetResult.OVERRIDE_SUCCESS - except NoResultFound: - result = SetResult.NEW_LINK_SUCCESS - session.add(Link(key=key, channel=link_channel, value=value)) - session.commit() - session.close() - return result, value - - -def get_link_value(key: str, - channel: str, - link_scope: Optional[LinkScope] = None) -> Tuple[Optional[str], Optional[str]]: - """ - Gets the value associated with a given key (and optionally channel). If a channel association - exists, this is returned, otherwise a global association is returned. If no association exists - then None is returned. The default behaviour can be overridden by passing the global flag to - force retrieval of a global association when a channel association exists. - :param key: the key to look up - :param channel: the name of the channel the lookup request was made from - :param link_scope: the requested scope to retrieve the link from (if supplied) - :return: the associated value if an association exists, else None, and the source - (global/channel) if any else None - """ - session = bot.create_db_session() - channel_match = session.query(Link).filter(Link.key == key, - Link.channel == channel).one_or_none() - global_match = session.query(Link).filter(Link.key == key, - Link.channel == None).one_or_none() # noqa: E711 - session.close() - - if link_scope == LinkScope.GLOBAL: - return (global_match.value, "global") if global_match else (None, None) - - if link_scope == LinkScope.CHANNEL: - return (channel_match.value, "channel") if channel_match else (None, None) - - if channel_match: - return channel_match.value, "channel" - - if global_match: - return global_match.value, "global" - - return None, None - - -@bot.on_command('link') -@loading_status -def handle_link(command: Command) -> None: - """ - `!link [-c | -g] [-f] key [value [value ...]]` - Set and retrieve information in a key value - store. Links can be set to be channel specific or global. Links are set as global by default, - and channel specific links are retrieved by default unless overridden with the respective flag. - """ - parser = ArgumentParser("!link", add_help=False) - parser.add_argument("key", type=str, help="Lookup key") - parser.add_argument("value", type=str, help="Value to associate with key", nargs="*") - flag_group = parser.add_mutually_exclusive_group() - flag_group.add_argument("-c", "--channel", action="store_true", dest="channel_flag", - help="Ensure a channel link is retrieved, or none is") - flag_group.add_argument("-g", "--global", action="store_true", dest="global_flag", - help="Ignore channel link and force retrieval of global") - parser.add_argument("-f", "--force-override", action="store_true", dest="override", - help="Must be passed if overriding a link") - - try: - args = parser.parse_args(command.arg.split() if command.has_arg() else []) - except SystemExit: - # Incorrect Usage - return bot.post_message(command.channel_id, "", - attachments=[Attachment(SectionBlock(str(parser.format_help())), - color=Color.YELLOW)._resolve()]) - - channel = bot.channels.get(command.channel_id) - if not channel: - return bot.post_message(command.channel_id, "", attachments=[ - Attachment(SectionBlock("Cannot find channel name, please try again."), - color=Color.YELLOW)._resolve() - ]) - - channel_name = channel.name - - link_scope = LinkScope.CHANNEL if args.channel_flag else \ - LinkScope.GLOBAL if args.global_flag else None - - # Retrieve a link - if not args.value: - link_value, source = get_link_value(key=args.key, - channel=channel_name, - link_scope=link_scope) - channel_text = f" in channel `{channel_name}`" if args.channel_flag else "" - if link_value: - source_text = source if source == 'global' else channel_name - response = f"{args.key} ({source_text}): {link_value}" - else: - response = f"No link found for key: `{args.key}`" + channel_text - color = Color.GREEN if link_value else Color.RED - return bot.post_message(command.channel_id, "", attachments=[ - Attachment(SectionBlock(response), color=color)._resolve() - ]) - - # Set a link - if args.key and args.value: - result, current_value = set_link_value(key=args.key, - channel=channel_name, - value=" ".join(args.value), - override=args.override, - link_scope=link_scope) - color = Color.YELLOW if result == SetResult.NEEDS_OVERRIDE else Color.GREEN - scope = channel_name if args.channel_flag else 'global' - response = f"{args.key} ({scope}): {current_value}" - attachment = Attachment(SectionBlock(response), color=color)._resolve() - bot.post_message(command.channel_id, f"{result.value}:", attachments=[attachment]) diff --git a/unimplemented/pastexams.py b/unimplemented/pastexams.py deleted file mode 100644 index 21cfa759..00000000 --- a/unimplemented/pastexams.py +++ /dev/null @@ -1,67 +0,0 @@ -from uqcsbot import bot, Command -from bs4 import BeautifulSoup -from typing import Iterable, Tuple -import requests -from uqcsbot.utils.command_utils import loading_status - - -@bot.on_command('pastexams') -@loading_status -def handle_pastexams(command: Command): - """ - `!pastexams [COURSE CODE]` - Retrieves past exams for a 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_code = command.arg if command.has_arg() else channel.name - bot.post_message(channel, get_past_exams(course_code)) - - -def get_exam_data(soup: BeautifulSoup) -> Iterable[Tuple[str, str]]: - """ - Takes the soup object of the page and generates each result in the format: - ('year Sem X:', link) - """ - - # The exams are stored in a table with the structure: - # Row 1: A bunch of informational text - # Row 2: Semester information - # Row 3: Links to Exams - # Rows two and three are what we care about. Of those the first column is just a row title so - # we ignore that as well - - exam_table_rows = soup.find('table', class_='maintable').contents - semesters = exam_table_rows[1].find_all('td')[1:] # All columns in row 2 excluding the first - # Gets the content from each td. Text is separated by a
thus result is in the format - # (year,
, 'Sem.x' - semesters = [semester.contents for semester in semesters] - - # Same thing but for links - links = exam_table_rows[2].find_all('td')[1:] - links = [link.find('a')['href'] for link in links] - - for (year, _, semester_id), link in zip(semesters, links): - semester_str = semester_id.replace('.', ' ') - yield f'{year} {semester_str}', link - - -def get_past_exams(course_code: str) -> str: - """ - Gets the past exams for the course with the specified course code. - Returns intuitive error messages if this fails. - """ - url = 'https://www.library.uq.edu.au/exams/papers.php?' - http_response = requests.get(url, params={'stub': course_code}) - - if http_response.status_code != requests.codes.ok: - return "There was a problem getting a response" - - # Check if the course code exists - soup = BeautifulSoup(http_response.content, 'html.parser') - no_course = soup.find('div', class_='page').find('div').contents[0] - if "Sorry. We have not found any past exams for this course" in no_course: - return f"The course code {course_code} did not return any results" - - return '>>>' + '\n'.join((f'*{semester}*: <{link}|PDF>' - for semester, link in get_exam_data(soup))) diff --git a/unimplemented/wavie.py b/unimplemented/wavie.py deleted file mode 100644 index be81b47f..00000000 --- a/unimplemented/wavie.py +++ /dev/null @@ -1,22 +0,0 @@ -from uqcsbot import bot -import logging - - -logger = logging.getLogger(__name__) - - -@bot.on('message') -def wave(evt): - """ - :wave: reacts to "person joined/left this channel" - - @no_help - """ - if evt.get('subtype') not in ['channel_join', 'channel_leave']: - return - chan = bot.channels.get(evt['channel']) - if chan is not None and chan.name == 'announcements': - return - result = bot.api.reactions.add(name='wave', channel=chan.id, timestamp=evt['ts']) - if not result.get('ok'): - logger.error(f"Error adding reaction: {result}") diff --git a/unimplemented/whoami.py b/unimplemented/whoami.py deleted file mode 100644 index 0d1983b7..00000000 --- a/unimplemented/whoami.py +++ /dev/null @@ -1,18 +0,0 @@ -from uqcsbot import bot, Command -from uqcsbot.utils.command_utils import success_status - - -@bot.on_command("whoami") -@success_status -def handle_whoami(command: Command): - """ - `!whoami` - Returns the Slack information for the calling user. - """ - response = bot.api.users.info(user=command.user_id) - if not response['ok']: - message = 'An error occurred, please try again.' - else: - user_info = response['user'] - message = f'Your vital statistics: \n```{user_info}```' - user_direct_channel = bot.channels.get(command.user_id) - bot.post_message(user_direct_channel, message) diff --git a/unimplemented/xkcd.py b/unimplemented/xkcd.py deleted file mode 100644 index eaf491a6..00000000 --- a/unimplemented/xkcd.py +++ /dev/null @@ -1,125 +0,0 @@ -import datetime -import requests -import feedparser -import re -from urllib.parse import quote -from uqcsbot import bot, Command -from uqcsbot.utils.command_utils import loading_status - - -# HTTP Endpoints -XKCD_BASE_URL = "https://xkcd.com/" -XKCD_RSS_URL = "https://xkcd.com/rss.xml" -RELEVANT_XKCD_URL = 'https://relevantxkcd.appspot.com/process' - - -def get_by_id(comic_number: int) -> str: - """ - Gets an xkcd comic based on its unique ID/sequence number. - :param comic_number: the ID number of the xkcd comic to retrieve. - :return: a response containing either a comic URL or an error message. - """ - if comic_number <= 0: - return "Invalid xkcd ID, it must be a positive integer." - url = f"{XKCD_BASE_URL}{str(comic_number)}" - response = requests.get(url) - if response.status_code != 200: - return "Could not retrieve an xkcd with that ID (are there even that many?)" - return url - - -def get_by_search_phrase(search_phrase: str) -> str: - """ - Uses the site relevantxkcd.appspot.com to identify the - most appropriate xkcd comic based on the phrase provided. - :param search_phrase: the phrase to find an xkcd comic related to. - :return: the URL of the most relevant comic for that search phrase. - """ - params = {"action": "xkcd", "query": quote(search_phrase)} - response = requests.get(RELEVANT_XKCD_URL, params=params) - # Response consists of a newline delimited list, with two irrelevant first parameters - relevant_comics = response.content.decode().split("\n")[2:] - # Each line consists of "comic_id image_url" - best_response = relevant_comics[0].split(" ") - comic_number = int(best_response[0]) - return get_by_id(comic_number) - - -def get_latest() -> str: - """ - Gets the most recently published xkcd comic by examining the RSS feed. - :return: the URL to the latest xkcd comic. - """ - rss = feedparser.parse(XKCD_RSS_URL) - entries = rss['entries'] - if len(entries) > 0: - i = 0 - latest = entries[i]['guid'] - if not re.match(r"https://xkcd\.com/\d+/", latest): - i += 1 - latest = entries[i]['guid'] - else: - latest = 'https://xkcd.com/2200/' - return latest - - -def is_id(argument: str) -> bool: - """ - Determines whether the given argument is a valid id (i.e. an integer). - :param argument: the string argument to evaluate - :return: true if the argument can be evaluated as an interger, false otherwise - """ - try: - int(argument) - except ValueError: - return False - else: - return True - - -@bot.on_command('xkcd') -@loading_status -def handle_xkcd(command: Command) -> None: - """ - `!xkcd [COMIC_ID|SEARCH_PHRASE]` - Returns the xkcd comic associated - with the given COMIC_ID (an integer) or matching the SEARCH_PHRASE. - Providing no arguments will return the most recent comic. - """ - if command.has_arg(): - argument = command.arg - if is_id(argument): - comic_number = int(argument) - response = get_by_id(comic_number) - else: - response = get_by_search_phrase(command.arg) - else: - response = get_latest() - - bot.post_message(command.channel_id, response, unfurl_links=True, unfurl_media=True) - - -@bot.on_schedule('cron', hour=14, minute=1, day_of_week='mon,wed,fri', - timezone='Australia/Brisbane') -def new_xkcd() -> None: - """ - Posts new xkcd comic when they are released every Monday, - Wednesday & Friday at 4AM UTC or 2PM Brisbane time. - - @no_help - """ - link = get_latest() - - day = datetime.datetime.today().weekday() - if (day == 0): # Monday - message = "It's Monday, 4 days till Friday; here's the" - elif (day == 2): # Wednesday - message = "Half way through the week, time for the" - elif (day == 4): # Friday - message = (":musical_note: It's Friday, Friday\nGotta get down on Friday\n" - "Everybody's lookin' forward to the") - else: - message = "@pah It is day " + str(day) + ", please fix me... Here's the" - message += " latest xkcd comic " - - general = bot.channels.get("general") - bot.post_message(general.id, message + link, unfurl_links=True, unfurl_media=True)