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

Demo - Room reservation #47

Merged
merged 10 commits into from
Aug 28, 2024
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
35 changes: 18 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,24 @@ projects for:
- Composable templates for interoperability between common services
- Demonstrations of system capabilities and features

| Name | Description | Integrations |
| :------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------- | :--------------------------------- |
| 🐍 [AWS Health to Slack](./aws_health_to_slack/) | Announce cloud health events based on a resource ownership mapping | AWS (Health), Google Sheets, Slack |
| 🐍 [Break-glass](./break_glass/) | Manage break-glass requests and approvals for temporary elevated permissions | Slack → AWS (IAM), Jira |
| 🐍 [Categorize emails](./categorize_emails/) | Categorize new emails and notify the appropriate channels based on the content | Gmail → ChatGPT → Slack |
| 🐍 [Confluence to Slack](./confluence_to_slack/) | Notify when a new page with a specific label is created | Confluence → Slack |
| 🐍 [Create Jira issue via webhook](./create_jira_issue/) | Create Jira issues with HTTP GET/POST requests | HTTP → Jira |
| 🐍 [Data pipeline](./data_pipeline/) | Process and store data from new S3 files in a database | AWS (SNS, S3) → SQLite |
| ⭐ [GitHub Copilot seats](./github_copilot/) | Automate daily GitHub Copilot user pruning and report changes | GitHub ↔ Slack |
| 🐍 [Google Forms to Jira](./google_forms_to_jira/) | Poll a form for responses and create an issue for each one | Google Forms → Jira |
| 🐍 [Jira assignee from schedule](./jira_google_calendar/assignee_from_schedule/) | Assign new Jira issues to the current on-caller based on a schedule in a shared calendar | Jira ↔ Google Calendar |
| 🐍 [Jira deadline to event](./jira_google_calendar/deadline_to_event/) | Create/update calendar events based on the deadlines of Jira issues | Jira ↔ Google Calendar |
| 🐍 [Quickstart](./quickstart/) | Basic workflow for tutorials | HTTP |
| ⭐ [Pull Request Review Reminder (Purrr)](./purrr/) | Streamline code reviews and cut down turnaround time to merge pull requests | GitHub ↔ Slack |
| ⭐ [ReviewKitteh](./reviewkitteh/) | Monitor pull requests, and meow at random people | GitHub, Google Sheets, Slack |
| 🐍 [Task chain](./task_chain/) | Run a sequence of tasks with fault tolerance | Slack |
| 🐍 [Slack Support](./slack_support/) | Categorize slack support requests using AI and make sure appropiate people handle them | Slack, Google Sheets, Gemini |
| Name | Description | Integrations |
| :------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------- | :------------------------------------------ |
| 🐍 [AWS Health to Slack](./aws_health_to_slack/) | Announce cloud health events based on a resource ownership mapping | AWS (Health), Google Sheets, Slack |
| 🐍 [Break-glass](./break_glass/) | Manage break-glass requests and approvals for temporary elevated permissions | Slack → AWS (IAM), Jira |
| 🐍 [Categorize emails](./categorize_emails/) | Categorize new emails and notify the appropriate channels based on the content | Gmail → ChatGPT → Slack |
| 🐍 [Confluence to Slack](./confluence_to_slack/) | Notify when a new page with a specific label is created | Confluence → Slack |
| 🐍 [Create Jira issue via webhook](./create_jira_issue/) | Create Jira issues with HTTP GET/POST requests | HTTP → Jira |
| 🐍 [Data pipeline](./data_pipeline/) | Process and store data from new S3 files in a database | AWS (SNS, S3) → SQLite |
| ⭐ [GitHub Copilot seats](./github_copilot/) | Automate daily GitHub Copilot user pruning and report changes | GitHub ↔ Slack |
| 🐍 [Google Forms to Jira](./google_forms_to_jira/) | Poll a form for responses and create an issue for each one | Google Forms → Jira |
| 🐍 [Jira assignee from schedule](./jira_google_calendar/assignee_from_schedule/) | Assign new Jira issues to the current on-caller based on a schedule in a shared calendar | Jira ↔ Google Calendar |
| 🐍 [Jira deadline to event](./jira_google_calendar/deadline_to_event/) | Create/update calendar events based on the deadlines of Jira issues | Jira ↔ Google Calendar |
| ⭐ [Pull Request Review Reminder (Purrr)](./purrr/) | Streamline code reviews and cut down turnaround time to merge pull requests | GitHub ↔ Slack |
| 🐍 [Quickstart](./quickstart/) | Basic workflow for tutorials | HTTP |
| ⭐ [ReviewKitteh](./reviewkitteh/) | Monitor pull requests, and meow at random people | GitHub, Google Sheets, Slack |
| 🐍 [Room reservation](./room_reservation/) | Manage via Slack ad-hoc room reservations in Google Calendar | Slack ↔ Google Calendar, Google Sheets |
| 🐍 [Slack support](./slack_support/) | Categorize Slack support requests using AI, and route them to the appropiate people | Slack ↔ Gemini, Google Sheets |
| 🐍 [Task chain](./task_chain/) | Run a sequence of tasks with fault tolerance | Slack |

> [!NOTE]
> 🐍 = Python implementation, ⭐ = Starlark implementation.
43 changes: 43 additions & 0 deletions room_reservation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Room Reservarion

In our organization, we manage meeting rooms as resources in Google Calendar:

- Each meeting room is represented by an email account
- To reserve a room for a meeting, users can add it to the invite

While users can reserve rooms directly from the calendar, we wanted to add a
Slack interface to make it even quicker to reserve a room for an immediate
meeting within the next half hour.

We configured three slash commands in Slack:

- `/availablerooms` - list all the available rooms
- `/roomstatus <room>` - check the status of a specific room
- `/reserveroom <room> <title>` - reserve a specific room

> [!TIP]
> You can extend this project to add participants, set the time, etc.

## Configuration

Each meeting room is represented by an email account.

The list of available meeting rooms is stored in a Google Sheet as a single
column of room email addresses:

| | A |
| :-: | :-----------------: |
| 1 | `[email protected]` |
| 2 | `[email protected]` |
| 3 | `[email protected]` |

Before deploying this project, set the `GOOGLE_SHEET_ID` variable in the
[autokitteh.yaml](./autokitteh.yaml) manifest file, to point to your Google
Sheet.

> [!TIP]
> You can extend this project to add another column for user-friendly aliases.

## AutoKitteh Integrations

Google Calendar, Google Sheets, Slack
40 changes: 40 additions & 0 deletions room_reservation/autokitteh.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# This YAML file is a declarative manifest that describes the setup
# of an AutoKitteh project that manages via Slack ad-hoc room
# reservations in Google Calendar.
#
# After creating this AutoKitteh project by applying this file,
# initialize its Google Calendar, Google Sheets, and Slack connections.
#
# Before deploying this AutoKitteh project, set the GOOGLE_SHEET_ID
# variable to point to a Google Sheet, as described in the README.md file.

version: v1

project:
name: room_reservation
vars:
- name: GOOGLE_SHEET_ID
value:
connections:
- name: calendar_conn
integration: googlecalendar
- name: sheets_conn
integration: googlesheets
- name: slack_conn
integration: slack
triggers:
- name: slack_slash_command_available_rooms
connection: slack_conn
event_type: slash_command
filter: data.command == "/availablerooms"
call: available_rooms.py:on_slack_slash_command
- name: slack_slash_command_room_status
connection: slack_conn
event_type: slash_command
filter: data.command == "/roomstatus"
call: room_status.py:on_slack_slash_command
- name: slack_slash_command_reserve_room
connection: slack_conn
event_type: slash_command
filter: data.command == "/reserveroom"
call: reserve_room.py:on_slack_slash_command
51 changes: 51 additions & 0 deletions room_reservation/available_rooms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""List all the available rooms for the next half hour."""

from datetime import UTC, datetime, timedelta

from autokitteh.google import google_calendar_client
from autokitteh.slack import slack_client
from googleapiclient.errors import HttpError

import google_sheets


def on_slack_slash_command(event):
"""Entry point for the "/availablerooms" Slack slash command."""
slack = slack_client("slack_conn")
channel_id = event.data.user_id # event.data.channel_id

now = datetime.now(UTC)
in_30_minutes = now + timedelta(minutes=30)
gcal = google_calendar_client("calendar_conn").events()

# Iterate over the list of rooms, notify the user about
# each room which is available in the next half hour.
available = False
for room in sorted(google_sheets.get_room_list()):
print(f"Checking upcoming events in: {room}")
try:
events = gcal.list(
calendarId=room,
timeMin=now.isoformat(),
timeMax=in_30_minutes.isoformat(),
singleEvents=True,
orderBy="startTime",
).execute()

events = events.get("items", [])
# Ignore non-blocking events where the room is marked as "free".
events = [e for e in events if e.get("transparency", "") != "transparent"]

if not events:
msg = f"The room `{room}` is available for the next half hour"
slack.chat_postMessage(channel=channel_id, text=msg)
available = True

except HttpError as e:
err = f"Error for the room `{room}`: '{e.reason}'"
slack.chat_postMessage(channel=channel_id, text=err)
print(f"Error when listing events for room `{room}`: {e}")

if not available:
msg = "No available rooms found for the next half hour"
slack.chat_postMessage(channel=channel_id, text=msg)
9 changes: 9 additions & 0 deletions room_reservation/google_sheets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import os

from autokitteh.google import google_sheets_client


def get_room_list():
sheet = google_sheets_client("sheets_conn").spreadsheets().values()
rows = sheet.get(spreadsheetId=os.getenv("GOOGLE_SHEET_ID"), range="A:A").execute()
return [cell[0] for cell in rows.get("values", []) if cell]
60 changes: 60 additions & 0 deletions room_reservation/reserve_room.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Reserve a specific room for the next half hour."""

from datetime import UTC, datetime, timedelta

import autokitteh
from autokitteh.google import google_calendar_client
from autokitteh.slack import slack_client
from googleapiclient.errors import HttpError

import google_sheets


def on_slack_slash_command(event):
"""Entry point for the "/reserveroom <room> <title>" Slack slash command."""
slack = slack_client("slack_conn")
channel_id = event.data.user_id # event.data.channel_id

cmd_text = event.data.text.split(maxsplit=1)
if len(cmd_text) < 2:
err = "Error: please use the following format: `/reserveroom <room> <title>`"
slack.chat_postMessage(channel=channel_id, text=err)
return

room, title = cmd_text
if room not in google_sheets.get_room_list():
err = f"Error: `{room}` not found in the list of rooms"
slack.chat_postMessage(channel=channel_id, text=err)
return

user = slack.users_profile_get(user=event.data.user_id).get("profile")

now = datetime.now(UTC)
in_5_minutes = now + timedelta(minutes=5)
in_30_minutes = now + timedelta(minutes=30)

event = {
"summary": title,
"description": f"Reserved via Slack by {user['real_name']}",
"start": {"dateTime": in_5_minutes.isoformat()},
"end": {"dateTime": in_30_minutes.isoformat()},
"reminders": {"useDefault": False},
"attendees": [
{"email": user["email"]},
],
}

result = _create_calendar_event(room, event)
slack.chat_postMessage(channel=channel_id, text=result)


# It's better to mark this as a single activity, to minimize the creation of
# multiple overly-granular activities during this Google Calendar API call.
@autokitteh.activity
def _create_calendar_event(room, event):
try:
gcal = google_calendar_client("calendar_conn").events()
gcal.insert(calendarId=room, body=event).execute()
return f"Scheduled a meeting now in the room `{room}`"
except HttpError as e:
return f"Error: failed to schedule a meeting ('{e.reason}')"
53 changes: 53 additions & 0 deletions room_reservation/room_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Check the status of a specific room for the next hour."""

from datetime import UTC, datetime, timedelta

import autokitteh
from autokitteh.google import google_calendar_client
from autokitteh.slack import slack_client
from googleapiclient.errors import HttpError

import google_sheets


def on_slack_slash_command(event):
"""Entry point for the "/roomstatus <room>" Slack slash command."""
slack = slack_client("slack_conn")
channel_id = event.data.user_id # event.data.channel_id

room = event.data.text
if room not in google_sheets.get_room_list():
err = f"Error: `{room}` not found in the list of rooms"
slack.chat_postMessage(channel=channel_id, text=err)

gcal = google_calendar_client("calendar_conn").events()
now = datetime.now(UTC)
in_1_hour = now + timedelta(hours=1)

msg = f"Events in the room `{room}`:"
try:
events = gcal.list(
calendarId=room,
timeMin=now.isoformat(),
timeMax=in_1_hour.isoformat(),
singleEvents=True,
orderBy="startTime",
).execute()

events = events.get("items", [])
# Ignore non-blocking events where the room is marked as "free".
events = [e for e in events if e.get("transparency", "") != "transparent"]

if not events:
msg += " none found for the next half hour"

for event in events:
event = autokitteh.AttrDict(event)
daabr marked this conversation as resolved.
Show resolved Hide resolved
start = event.start.get("dateTime") or event.start.get("date")
msg += f"\n{start} - {event.summary}"

except HttpError as e:
msg += f" error '{e.reason}'"
print(f"Error when listing events for room `{room}`: {e}")

slack.chat_postMessage(channel=channel_id, text=msg)