From f3060c080671445bf3b432c7f820886089a314c7 Mon Sep 17 00:00:00 2001 From: Tony Breeds Date: Wed, 7 Jun 2023 09:29:33 -0500 Subject: [PATCH] [iCal] Add an iCal file route. Add a handler that converts the existing slot data into an ICS file. This .ics file can be global ".../ptg.ics" or per track ".../ironic.ics" Right now each block deafults to 60 minutes if there isn't a 'duration' field in the slot entry. Change-Id: I7a67ba777c004e57206976aca377fe3f41956db0 --- ptgbot/ics.py | 66 ++++++++++++++++++++++++++++++++++++++++++++++++ ptgbot/web.py | 11 ++++++++ requirements.txt | 1 + 3 files changed, 78 insertions(+) create mode 100644 ptgbot/ics.py diff --git a/ptgbot/ics.py b/ptgbot/ics.py new file mode 100644 index 0000000..370486f --- /dev/null +++ b/ptgbot/ics.py @@ -0,0 +1,66 @@ + +import datetime +import icalendar + + +def json2ical(db, include_teams="ALL"): + teams = slots = {} + + # FIXME: The unused label and the _slots here are irritating. + for label, _slots in db["slots"].items(): + for slot in _slots: + slots[slot["name"]] = slot + + for location, schedule in db["schedule"].items(): + for slot in schedule: + team = schedule[slot] + # FIXME: This is possibly wrong as teams can globally override the + # VC url and ignore the setting in the room/slot/table + if slot == "url" or team == "": + continue + teams.setdefault(team, []).append((location, slot)) + + if include_teams in ["ALL", "ptg"]: + include_teams = list(teams.keys()) + if isinstance(include_teams, str): + include_teams = [include_teams] + + c = icalendar.Calendar() + c.add("prodid", "-//Opendev PTGBot//ptg.opendev.org//") + c.add("version", "2.0") + + for team in include_teams: + for booking in teams.get(team, []): + location, slot = booking + url = db["schedule"].get(location, {}).get("url", "") + etherpad = db["etherpads"].get(team, "") + time = slots.get(slot, {}).get("realtime") + # TODO(tonyb): 60 mins is a default picked to make the existing + # DB work unchanged. We can leave this as is or pick another + # number. Longer term we could also potentially add a + # 'default_duration' to the DB as a per-event not hard-coded + # value to save adding a 'duration' to each slot. + duration = slots.get(slot, {}).get("duration", 60) + dtstart = datetime.datetime.fromisoformat(time) + name = summary = "[PTG] " + team + desc = "Etherpad: " + etherpad + "\n" + uid = (dtstart.strftime("%Y%m%d%H%M") + "/" + location + + "@ptg.opendev.org") + + e = icalendar.Event() + e.add("name", name) + e.add("summary", summary) + e.add("description", desc) + e.add("dtstart", dtstart) + e.add("dtend", dtstart + datetime.timedelta(mins=duration)) + e.add("priority", 0) + + # FIXME: Why the change in format + # FIXME: Check for empty url? it shouldn't happen due to the + # way we build the the teams/bookings lists + e["location"] = icalendar.vText(url) + e["uid"] = uid + + c.add_component(e) + + return c.to_ical() diff --git a/ptgbot/web.py b/ptgbot/web.py index 938f3ba..b5b276c 100644 --- a/ptgbot/web.py +++ b/ptgbot/web.py @@ -8,6 +8,9 @@ import os import pkg_resources import socketserver +import ptgbot.ics + + CONFIG = {} @@ -19,6 +22,14 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler): self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(fp.read()) + elif self.path.endswith('.ics'): + team = os.path.basename(self.path)[:-4] + with open(CONFIG['db_filename'], 'rb') as fp: + ics = ptgbot.ics.json2ical(json.load(fp), team) + self.send_response(200) + self.send_header('Content-type', 'text/calendar') + self.end_headers() + self.wfile.write(ics) else: http.server.SimpleHTTPRequestHandler.do_GET(self) diff --git a/requirements.txt b/requirements.txt index 9147466..9ae2beb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ irc==15.1.1 python-daemon >= 1.6 ib3 requests +icalendar >= 5.0.0