diff --git a/src/bot.py b/src/bot.py index a051caa..570420a 100644 --- a/src/bot.py +++ b/src/bot.py @@ -35,74 +35,59 @@ APP_HANDLER = AsyncSlackRequestHandler(APP) -async def periodically_check_api(): - """Periodically check the api every hour - - This function runs in a thread, meaning that it needs to create it's own - database connection. This is OK however, since it only runs once an hour - """ - print("Checking api every hour") - while True: - try: - await check_api() - except Exception: # pylint: disable=broad-except - print(traceback.format_exc()) - os._exit(1) - await asyncio.sleep(60 * 60) # 60 minutes x 60 seconds +async def post_or_update_messages(week, blocks, text): + """Posts or updates a message in a slack channel for a week""" + channels = await database.get_slack_channel_ids() + messages = await database.get_messages(week) + # used to lookup the message id and message for a particular + # channel + message_details = { + message["slack_channel_id"]: { + "timestamp": message["message_timestamp"], + "message": message["message"], + } + for message in messages + } -@APP.command("/add_channel") -async def add_channel(ack, say, logger, command): - """Handle adding a slack channel to the bot""" - del say - logger.info(f"{command['command']} from {command['channel_id']}") - if command["channel_id"] is not None: - try: - await database.add_channel(command["channel_id"]) - await ack("Added channel to slack events bot 👍") - except sqlite3.IntegrityError: - await ack("slack events bot has already been activated for this channel") + # used to quickly lookup if a message has been posted for a + # particular channel + posted_channels_set = set(message["slack_channel_id"] for message in messages) + for slack_channel_id in channels: + if ( + slack_channel_id in posted_channels_set + and text == message_details[slack_channel_id]["message"] + ): + print( + f"Week of {week.strftime('%B %-d')} in {slack_channel_id} " + "hasn't changed, not updating" + ) -@APP.command("/remove_channel") -async def remove_channel(ack, say, logger, command): - """Handle removing a slack channel from the bot""" - del say - logger.info(f"{command['command']} from {command['channel_id']}") - if command["channel_id"] is not None: - try: - await database.remove_channel(command["channel_id"]) - await ack("Removed channel from slack events bot 👍") - except sqlite3.IntegrityError: - await ack("slack events bot is not activated for this channel") + elif slack_channel_id in posted_channels_set: + print(f"updating week {week.strftime('%B %-d')} " f"in {slack_channel_id}") + timestamp = message_details[slack_channel_id]["timestamp"] + slack_response = await APP.client.chat_update( + ts=timestamp, channel=slack_channel_id, blocks=blocks, text=text + ) -@APP.command("/check_api") -async def trigger_check_api(ack, say, logger, command): - """Handle manually rechecking the api for updates""" - del say - logger.info(f"{command['command']} from {command['channel_id']}") - if command["channel_id"] is not None: - await ack("Checking api for events 👍") - await check_api() + await database.update_message(week, text, timestamp, slack_channel_id) + else: + print(f"posting week {week.strftime('%B %-d')} " f"in {slack_channel_id}") -async def check_api(): - """Check the api for updates and update any existing messages""" - async with aiohttp.ClientSession() as session: - async with session.get("https://events.openupstate.org/api/gtc") as resp: - # get timezone aware today - today = datetime.date.today() - today = datetime.datetime( - today.year, today.month, today.day, tzinfo=pytz.utc + slack_response = await APP.client.chat_postMessage( + channel=slack_channel_id, + blocks=blocks, + text=text, + unfurl_links=False, + unfurl_media=False, ) - # keep current week's post up to date - await parse_events_for_week(today, resp) - - # potentially post next week 5 days early - probe_date = today + datetime.timedelta(days=5) - await parse_events_for_week(probe_date, resp) + await database.create_message( + week, text, slack_response["ts"], slack_channel_id + ) async def parse_events_for_week(probe_date, resp): @@ -147,59 +132,74 @@ async def parse_events_for_week(probe_date, resp): await post_or_update_messages(week_start, blocks, text) -async def post_or_update_messages(week, blocks, text): - """Posts or updates a message in a slack channel for a week""" - channels = await database.get_slack_channel_ids() - messages = await database.get_messages(week) +async def check_api(): + """Check the api for updates and update any existing messages""" + async with aiohttp.ClientSession() as session: + async with session.get("https://events.openupstate.org/api/gtc") as resp: + # get timezone aware today + today = datetime.date.today() + today = datetime.datetime( + today.year, today.month, today.day, tzinfo=pytz.utc + ) - # used to lookup the message id and message for a particular - # channel - message_details = { - message["slack_channel_id"]: { - "timestamp": message["message_timestamp"], - "message": message["message"], - } - for message in messages - } + # keep current week's post up to date + await parse_events_for_week(today, resp) - # used to quickly lookup if a message has been posted for a - # particular channel - posted_channels_set = set(message["slack_channel_id"] for message in messages) + # potentially post next week 5 days early + probe_date = today + datetime.timedelta(days=5) + await parse_events_for_week(probe_date, resp) - for slack_channel_id in channels: - if ( - slack_channel_id in posted_channels_set - and text == message_details[slack_channel_id]["message"] - ): - print( - f"Week of {week.strftime('%B %-d')} in {slack_channel_id} " - "hasn't changed, not updating" - ) - elif slack_channel_id in posted_channels_set: - print(f"updating week {week.strftime('%B %-d')} " f"in {slack_channel_id}") +async def periodically_check_api(): + """Periodically check the api every hour - timestamp = message_details[slack_channel_id]["timestamp"] - slack_response = await APP.client.chat_update( - ts=timestamp, channel=slack_channel_id, blocks=blocks, text=text - ) + This function runs in a thread, meaning that it needs to create it's own + database connection. This is OK however, since it only runs once an hour + """ + print("Checking api every hour") + while True: + try: + await check_api() + except Exception: # pylint: disable=broad-except + print(traceback.format_exc()) + os._exit(1) + await asyncio.sleep(60 * 60) # 60 minutes x 60 seconds - await database.update_message(week, text, timestamp, slack_channel_id) - else: - print(f"posting week {week.strftime('%B %-d')} " f"in {slack_channel_id}") +@APP.command("/add_channel") +async def add_channel(ack, say, logger, command): + """Handle adding a slack channel to the bot""" + del say + logger.info(f"{command['command']} from {command['channel_id']}") + if command["channel_id"] is not None: + try: + await database.add_channel(command["channel_id"]) + await ack("Added channel to slack events bot 👍") + except sqlite3.IntegrityError: + await ack("slack events bot has already been activated for this channel") - slack_response = await APP.client.chat_postMessage( - channel=slack_channel_id, - blocks=blocks, - text=text, - unfurl_links=False, - unfurl_media=False, - ) - await database.create_message( - week, text, slack_response["ts"], slack_channel_id - ) +@APP.command("/remove_channel") +async def remove_channel(ack, say, logger, command): + """Handle removing a slack channel from the bot""" + del say + logger.info(f"{command['command']} from {command['channel_id']}") + if command["channel_id"] is not None: + try: + await database.remove_channel(command["channel_id"]) + await ack("Removed channel from slack events bot 👍") + except sqlite3.IntegrityError: + await ack("slack events bot is not activated for this channel") + + +@APP.command("/check_api") +async def trigger_check_api(ack, say, logger, command): + """Handle manually rechecking the api for updates""" + del say + logger.info(f"{command['command']} from {command['channel_id']}") + if command["channel_id"] is not None: + await ack("Checking api for events 👍") + await check_api() API = FastAPI() diff --git a/src/event.py b/src/event.py index 4fc5a83..140d743 100644 --- a/src/event.py +++ b/src/event.py @@ -7,6 +7,67 @@ from dateutil import parser +def parse_location(event_json): + """Parse location string from event json""" + if event_json["venue"] is None: + return None + + if None not in ( + event_json["venue"]["name"], + event_json["venue"]["address"], + event_json["venue"]["city"], + event_json["venue"]["state"], + event_json["venue"]["zip"], + ): + return ( + f"{event_json['venue']['name']} at " + f"{event_json['venue']['address']} {event_json['venue']['city']}, " + f"{event_json['venue']['state']} {event_json['venue']['zip']}" + ) + + if event_json["venue"]["lat"] is not None and event_json["venue"]["lat"]: + return f"lat/long: {event_json['venue']['lat']}, {event_json['venue']['lat']}" + + return f"{event_json['venue']['name']}" + + +def truncate_string(string, length=250): + """Truncate string and add ellipses if it's too long""" + return string[:length] + (string[length:] and "...") + + +def get_location_url(location): + """Return google maps link for location or plaintext""" + if location is None: + return "No location" + + return ( + "" + ) + + +def print_status(status): + """Prints status with emojis :D""" + if status == "upcoming": + return "Upcoming ✅" + + if status == "past": + return "Past ✔" + + if status == "cancelled": + return "Cancelled ❌" + + return status.title() + + +def print_datetime(time): + """Print datetime in local timezone as string""" + return time.astimezone(pytz.timezone(os.environ.get("TZ"))).strftime( + "%B %-d, %Y %I:%M %p %Z" + ) + + class Event: """Event records all the data from an event, and has methods to generate the message from an event @@ -79,64 +140,3 @@ def generate_text(self): f"Location: {self.location}\n" f"Time: {print_datetime(self.time)}" ) - - -def parse_location(event_json): - """Parse location string from event json""" - if event_json["venue"] is None: - return None - - if None not in ( - event_json["venue"]["name"], - event_json["venue"]["address"], - event_json["venue"]["city"], - event_json["venue"]["state"], - event_json["venue"]["zip"], - ): - return ( - f"{event_json['venue']['name']} at " - f"{event_json['venue']['address']} {event_json['venue']['city']}, " - f"{event_json['venue']['state']} {event_json['venue']['zip']}" - ) - - if event_json["venue"]["lat"] is not None and event_json["venue"]["lat"]: - return f"lat/long: {event_json['venue']['lat']}, {event_json['venue']['lat']}" - - return f"{event_json['venue']['name']}" - - -def truncate_string(string, length=250): - """Truncate string and add ellipses if it's too long""" - return string[:length] + (string[length:] and "...") - - -def get_location_url(location): - """Return google maps link for location or plaintext""" - if location is None: - return "No location" - - return ( - "" - ) - - -def print_status(status): - """Prints status with emojis :D""" - if status == "upcoming": - return "Upcoming ✅" - - if status == "past": - return "Past ✔" - - if status == "cancelled": - return "Cancelled ❌" - - return status.title() - - -def print_datetime(time): - """Print datetime in local timezone as string""" - return time.astimezone(pytz.timezone(os.environ.get("TZ"))).strftime( - "%B %-d, %Y %I:%M %p %Z" - )