Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More code formatting #40

Merged
merged 10 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ jobs:
- name: Black
run: python -m black --check src/

- name: isort
run: python -m isort --check src/

- name: ssort
run: python -m ssort --check src/

- name: Pylint
run: python -m pylint src/

Expand Down
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ The following needs to be included in an appropriate Apache .conf file, usually
1. Create a virtual environment with `python -m venv env`.
1. Activate the venv with `source env/bin/activate`
1. Use `deactivate` to exit the venv if needed.
1. Install dependencies using `pip install -r requirements.txt`
1. Install project dependencies using `pip install .` or `pip install .[test]`
to install development dependencies for testing
1. Run the app with `python src/bot.py`!

1. Proxy a web server to the running app's port, as defined in the .envrc `PORT` value.
Expand Down Expand Up @@ -136,10 +137,13 @@ good idea!
the source code in the `src/` folder.
- [pylint](https://pylint.readthedocs.io/en/stable/) via `pylint src/` to lint
the source code in the `src/` folder. We want this to stay at 10/10!
- `pipreqs --force` to save a new version of `requirements.txt`. This is only
necessary if you're adding or removing a new dependency. If you're updating
the requirements, make sure to add it to the list of dependencies in
`pyproject.toml` as well!
- [isort](https://pycqa.github.io/isort/index.html) via `isort src/` to make
sure that imports are in a standard order (black doesn't do this).
- [ssort](https://github.com/bwhmather/ssort) via `ssort src/` to better group
code.
- `pip freeze` to figure out which versions of dependencies to use in
`pyproject.toml`. This is only necessary if you're adding or removing a new
dependency to the project.

## License

Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ dependencies = [
test = [
"black==23.9.1",
"httpx==0.25.0",
"isort==5.12.0",
"pylint==2.17.5",
"pytest==7.4.2",
"pytest-asyncio==0.21.1"
"pytest-asyncio==0.21.1",
"ssort==0.11.6"
]

[project.urls]
Expand Down
215 changes: 107 additions & 108 deletions src/bot.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
"""The hackgreenville labs slack bot"""

from collections.abc import Awaitable, Callable
from typing import Union

import asyncio
import datetime
import logging
Expand All @@ -12,14 +9,16 @@
import sys
import threading
import traceback
from collections.abc import Awaitable, Callable
from typing import Union

import aiohttp
import pytz
import uvicorn

from fastapi import FastAPI, Request, HTTPException
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import PlainTextResponse
from slack_bolt.async_app import AsyncApp
from slack_bolt.adapter.fastapi.async_handler import AsyncSlackRequestHandler
from slack_bolt.async_app import AsyncApp
from starlette.types import Message

import database
Expand All @@ -36,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):
Expand Down Expand Up @@ -148,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()
Expand Down
4 changes: 2 additions & 2 deletions src/database.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Contains all the functions that interact with the sqlite database"""
import os
import datetime
import os
import sqlite3
from typing import Union, Generator
from typing import Generator, Union

DB_PATH = os.path.abspath(os.environ.get("DB_PATH", "./slack-events-bot.db"))

Expand Down
Loading