Skip to content

Instantly share code, notes, and snippets.

@hutt
Last active May 18, 2024 16:31
Show Gist options
  • Save hutt/89588365096c051e9375b8c667e8c6be to your computer and use it in GitHub Desktop.
Save hutt/89588365096c051e9375b8c667e8c6be to your computer and use it in GitHub Desktop.
Bundestags-Tagesordnung API: BT-TO als iCal, JSON & XML verfügbar machen. Alles mit einem Cloudflare-Worker. Web: https://api.hutt.io/bt-to/
/* *
* bt-to api
* hutt
* 2024-05-17
*
*/
import cheerio from "cheerio";
addEventListener("fetch", (event) => {
event.respondWith(handleRequest(event));
});
async function handleRequest(event) {
const request = event.request;
const url = new URL(request.url);
const path = url.pathname;
if (path === "/bt-to/" || path === "/bt-to") {
return serveDocumentation();
} else if (path === "/bt-to/ical" || path === "/bt-to/ics") {
return getOrFetchAgenda(event, "ical");
} else if (path === "/bt-to/json") {
return getOrFetchAgenda(event, "json");
} else if (path === "/bt-to/xml") {
return getOrFetchAgenda(event, "xml");
}
return new Response("Not Found", { status: 404 });
}
async function serveDocumentation() {
const html = `
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bundestag Tagesordnung API Dokumentation</title>
<meta name="robots" content="noindex,nofollow">
<style>
/* Reset some default browser styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Roboto', sans-serif;
line-height: 1.6;
background-color: #f7f9fc;
color: #333;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
min-height: 100vh;
}
.container {
max-width: 800px;
margin: auto;
padding: 20px;
flex-grow: 1;
}
header {
text-align: center;
padding: 20px 0;
background-color: #007acc;
color: #fff;
border-bottom: 4px solid #005ea1;
}
header h1 {
margin: 0;
font-size: 2.5em;
font-weight: 300;
}
h2, h3 {
color: #333;
margin-bottom: 10px;
font-weight: 400;
}
p {
margin-bottom: 20px;
}
a {
color: #007acc;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
ul {
margin: 20px 0;
padding-left: 20px;
}
li {
margin-bottom: 10px;
}
pre {
background: #f4f4f9;
padding: 15px;
border-radius: 8px;
border: 1px solid #e1e4e8;
overflow-x: auto;
font-size: 0.9em;
word-wrap: break-word;
white-space: pre-wrap;
}
code {
font-family: 'Source Code Pro', monospace;
white-space: pre-wrap;
display: block;
}
footer {
text-align: center;
padding: 10px 0;
background-color: #f1f3f5;
border-top: 1px solid #e1e4e8;
font-size: 0.9em;
}
footer p {
margin: 5px 0;
}
@media (max-width: 600px) {
.container {
padding: 25px;
}
header h1 {
font-size: 2em;
}
pre {
font-size: 0.8em;
}
}
/* Syntax Highlighting */
.token.comment { color: #6a737d; }
.token.punctuation { color: #24292e; }
.token.property { color: #005cc5; }
.token.selector { color: #005cc5; }
.token.operator { color: #d73a49; }
.token.keyword { color: #d73a49; }
.token.function { color: #6f42c1; }
.token.string { color: #032f62; }
.token.number { color: #005cc5; }
.token.boolean { color: #d73a49; }
/* Syntax Highlighting for JSON */
.language-json .token.property { color: #005cc5; } /* Keys */
.language-json .token.string { color: #032f62; } /* Values */
.language-json .token.number { color: #005cc5; } /* Numbers */
.language-json .token.boolean { color: #d73a49; } /* Booleans */
.language-json .token.null { color: #d73a49; } /* Null */
/* Syntax Highlighting for XML */
.language-xml .token.tag { color: #d73a49; } /* Tags */
.language-xml .token.attr-name { color: #6f42c1; } /* Attribute names */
.language-xml .token.attr-value { color: #032f62; } /* Attribute values */
.language-xml .token.punctuation { color: #24292e; } /* < > / = */
</style>
<link href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css" rel="stylesheet" />
</head>
<body>
<header>
<h1>BT-TO API-Dokumentation</h1>
</header>
<div class="container">
<p>Diese API stellt die Tagesordnung der aktuellen Sitzungswoche des Deutschen Bundestages in verschiedenen Formaten zur Verfügung. Die Tagesordnung wird alle 15 Minuten aktualisiert. Alles, was hier bisher zu sehen ist, ist <em>work in progress</em>.</p>
<p>Warum es diese Website überhaupt gibt? Nun ja, auf der offiziellen Website des Bundestages gibt es sogar eine Übersicht über den aktuellen Sitzungsverlauf und die besprochenen TOPs – nur leider in keinem maschinenlesbaren Format. Und so kommt es, dass mindestens 736 Büroleiter_innen in jeder Sitzungswoche ständig die Kalender ihrer MdB aktualisieren müssen – obwohl es doch so viel einfacher ginge. Das hat jetzt ein Ende. Einfach den iCal-Endpunkt als Kalender abonnieren und gut ist.</p>
<h2>API-Endpunkte</h2>
<ul>
<li><a href="/bt-to/ical">/bt-to/ical</a> - iCal-Format</li>
<li><a href="/bt-to/json">/bt-to/json</a> - JSON-Format</li>
<li><a href="/bt-to/xml">/bt-to/xml</a> - XML-Format</li>
</ul>
<h2>iCal-Endpunkt</h2>
<p>Der iCal-Endpunkt liefert die Tagesordnung im iCal-Format.</p>
<h3>Request</h3>
<pre><code class="language-http">GET https://api.hutt.io/bt-to/ical</code></pre>
<h3>Response</h3>
<p>Beispiel einer iCal-Antwort:</p>
<pre><code class="language-properties">&lt;BEGIN:VCALENDAR&gt;
&lt;VERSION:2.0&gt;
&lt;PRODID:-//hutt.io//api.hutt.io/bt-to//&gt;
&lt;CALSCALE:GREGORIAN&gt;
&lt;COLOR:#808080&gt;
&lt;X-APPLE-CALENDAR-COLOR:#808080&gt;
&lt;X-WR-CALNAME:Tagesordnung Bundestag&gt;
&lt;X-WR-CALDESC:Dieses iCal-Feed stellt die aktuelle Tagesordnung des
Plenums des Deutschen Bundestages zur Verfügung. Es aktualisiert
sich alle 15min selbst. Zwar ist der Sitzungsverlauf auch online
unter bundestag.de/tagesordnung einsehbar, doch leider werden
diese Daten nicht in einem maschinenlesbaren Format zur Verfügung
gestellt. Deshalb war es Zeit, das selbst in die Hand zu nehmen.
Mehr Informationen über das Projekt: https://api.hutt.io/bt-to/.&gt;
&lt;DESCRIPTION:Dieses iCal-Feed stellt die aktuelle Tagesordnung des
Plenums des Deutschen Bundestages zur Verfügung. Es aktualisiert
sich alle 15min selbst. Zwar ist der Sitzungsverlauf auch online
unter bundestag.de/tagesordnung einsehbar, doch leider werden
diese Daten nicht in einem maschinenlesbaren Format zur Verfügung
gestellt. Deshalb war es Zeit, das selbst in die Hand zu nehmen.
Mehr Informationen über das Projekt: https://api.hutt.io/bt-to/.&gt;
&lt;SOURCE;VALUE=URI:https://api.hutt.io/bt-to/ical&gt;
&lt;BEGIN:VTIMEZONE&gt;
&lt;TZID:Europe/Berlin&gt;
&lt;BEGIN:STANDARD&gt;
&lt;TZNAME:CET&gt;
&lt;DTSTART:19701025T030000&gt;
&lt;TZOFFSETFROM:+0200&gt;
&lt;TZOFFSETTO:+0100&gt;
&lt;RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU&gt;
&lt;/BEGIN:STANDARD&gt;
&lt;BEGIN:DAYLIGHT&gt;
&lt;TZNAME:CEST&gt;
&lt;DTSTART:19700329T020000&gt;
&lt;TZOFFSETFROM:+0100&gt;
&lt;TZOFFSETTO:+0200&gt;
&lt;RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU&gt;
&lt;/BEGIN:DAYLIGHT&gt;
&lt;/END:VTIMEZONE&gt;
&lt;BEGIN:VEVENT&gt;
&lt;UID:1715787900000-prävention-zur-verhinderung-der-pflegebedürftigkeit-4@api.hutt.io&gt;
&lt;DTSTAMP:20240517T185315Z&gt;
&lt;DTSTART:20240515T154500Z&gt;
&lt;DTEND:20240515T163500Z&gt;
&lt;SUMMARY:TOP 4: Prävention zur Verhinderung der Pflegebedürftigkeit&gt;
&lt;DESCRIPTION:Status: Überweisung 20/11152 beschlossen\n\nBeratung des
Antrags der Fraktion der CDU/CSU \nPflegebedürftigkeit frühestmöglich
verhindern – Gesundheitsförderung und Prävention in der Pflege stärken
\nDrucksache 20/11152&gt;
&lt;URL:https://bundestag.de/dokumente/textarchiv/2024/kw20-de-pflegebeduerftigkeit-1000384&gt;
&lt;/END:VEVENT&gt;
&lt;/END:VCALENDAR&gt;</code></pre>
<h2>JSON-Endpunkt</h2>
<p>Der JSON-Endpunkt liefert die Tagesordnung im JSON-Format.</p>
<h3>Request</h3>
<pre><code class="language-http">GET https://api.hutt.io/bt-to/json</code></pre>
<h3>Response</h3>
<p>Beispiel einer JSON-Antwort:</p>
<pre><code class="language-json">[
{
&quot;start&quot;: &quot;2024-05-15T15:45:00.000Z&quot;,
&quot;end&quot;: &quot;2024-05-15T16:35:00.000Z&quot;,
&quot;top&quot;: &quot;TOP 4&quot;,
&quot;thema&quot;: &quot;Prävention zur Verhinderung der Pflegebedürftigkeit&quot;,
&quot;beschreibung&quot;: &quot;Status: Überweisung 20/11152 beschlossen\n\nBeratung des Antrags der Fraktion der CDU/CSU \nPflegebedürftigkeit frühestmöglich verhindern – Gesundheitsförderung und Prävention in der Pflege stärken\nDrucksache 20/11152&quot;,
&quot;url&quot;: &quot;https://bundestag.de/dokumente/textarchiv/2024/kw20-de-pflegebeduerftigkeit-1000384&quot;,
&quot;status&quot;: &quot;Überweisung 20/11152 beschlossen&quot;,
&quot;uid&quot;: &quot;1715787900000-prävention-zur-verhinderung-der-pflegebedürftigkeit-4@api.hutt.io&quot;,
&quot;dtstamp&quot;: &quot;2024-05-17T18:50:54.469Z&quot;
}
]</code></pre>
<h2>XML-Endpunkt</h2>
<p>Der XML-Endpunkt liefert die Tagesordnung im XML-Format.</p>
<h3>Request</h3>
<pre><code class="language-http">GET https://api.hutt.io/bt-to/xml</code></pre>
<h3>Response</h3>
<p>Beispiel einer XML-Antwort:</p>
<pre><code class="language-xml">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;agenda&gt;
&lt;event&gt;
&lt;start&gt;2024-05-15T15:45:00.000Z&lt;/start&gt;
&lt;end&gt;2024-05-15T16:35:00.000Z&lt;/end&gt;
&lt;top&gt;TOP 4&lt;/top&gt;
&lt;thema&gt;
Prävention zur Verhinderung der Pflegebedürftigkeit
&lt;/thema&gt;
&lt;status&gt;Überweisung 20/11152 beschlossen&lt;/status&gt;
&lt;beschreibung&gt;
Status: Überweisung 20/11152 beschlossen Beratung des Antrags der Fraktion der CDU/CSU Pflegebedürftigkeit frühestmöglich verhindern – Gesundheitsförderung und Prävention in der Pflege stärken Drucksache 20/11152
&lt;/beschreibung&gt;
&lt;url&gt;
https://bundestag.de/dokumente/textarchiv/2024/kw20-de-pflegebeduerftigkeit-1000384
&lt;/url&gt;
&lt;/event&gt;
&lt;/agenda&gt;</code></pre>
<h2>Parameter</h2>
<p>Die folgenden Parameter sind in den API-Antworten enthalten:</p>
<ul>
<li><code>start</code> - Startzeitpunkt des Ereignisses (ISO 8601 Format)</li>
<li><code>end</code> - Endzeitpunkt des Ereignisses (ISO 8601 Format)</li>
<li><code>top</code> - TOP Nummer (wenn vorhanden)</li>
<li><code>thema</code> - Thema des Ereignisses</li>
<li><code>status</code> - Status des Ereignisses (wenn vorhanden)</li>
<li><code>beschreibung</code> - Beschreibung des Ereignisses</li>
<li><code>url</code> - URL zu weiteren Informationen (wenn vorhanden)</li>
</ul>
</div>
<footer>
<p>Kontakt: Jannis Hutt | <a href="mailto:jannis@hutt.io">jannis@hutt.io</a> | <a href="https://hutt.io">hutt.io</a> | <a href="https://gist.github.com/hutt/89588365096c051e9375b8c667e8c6be">Quellcode ansehen</a></p>
</footer>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-core.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
</body>
</html>
`;
return new Response(html, {
headers: { "content-type": "text/html; charset=UTF-8" },
});
}
async function getOrFetchAgenda(event, format) {
const cache = caches.default;
const cacheKey = new Request(event.request.url + format);
let response = await cache.match(cacheKey);
if (!response) {
const year = new Date().getFullYear();
const week = getWeekNumber(new Date());
const html = await fetchAgenda(year, week);
const agendaItems = parseAgenda(html);
response = formatAgendaResponse(format, agendaItems);
const cacheTtl = 15 * 60; // 15 minutes in seconds
response.headers.append("Cache-Control", `max-age=${cacheTtl}`);
event.waitUntil(cache.put(cacheKey, response.clone()));
}
return response;
}
function formatAgendaResponse(format, agendaItems) {
let data;
let contentType;
if (format === "ical") {
data = createIcal(agendaItems);
contentType = "text/calendar; charset=utf-8";
} else if (format === "json") {
data = JSON.stringify(agendaItems);
contentType = "application/json; charset=utf-8";
} else if (format === "xml") {
data = createXml(agendaItems);
contentType = "application/xml; charset=utf-8";
}
return new Response(data, {
headers: { "content-type": contentType },
});
}
async function fetchAgenda(year, week) {
const response = await fetch(
`https://www.bundestag.de/apps/plenar/plenar/conferenceweekDetail.form?year=${year}&week=${week}`,
);
if (!response.ok) {
throw new Error("Failed to fetch agenda");
}
return await response.text();
}
function parseAgenda(html) {
const $ = cheerio.load(html);
const tables = $("table.bt-table-data");
const agendaItems = [];
const months = {
Januar: 0, Februar: 1, März: 2, April: 3, Mai: 4, Juni: 5,
Juli: 6, August: 7, September: 8, Oktober: 9, November: 10, Dezember: 11
};
tables.each((_, table) => {
const dateStr = $(table).find("div.bt-conference-title").text().split("(")[0].trim();
const [day, monthName, year] = dateStr.split(" ");
const month = months[monthName];
const date = new Date(year, month, parseInt(day, 10));
const rows = $(table).find("tbody > tr");
for (let i = 1; i < rows.length - 1; i++) {
const startRow = rows[i];
const endRow = rows[i + 1];
const startTimeStr = $(startRow).find('td[data-th="Uhrzeit"]').text().trim();
const endTimeStr = $(endRow).find('td[data-th="Uhrzeit"]').text().trim();
const [startHour, startMinute] = startTimeStr.split(":").map(Number);
const [endHour, endMinute] = endTimeStr.split(":").map(Number);
const startDateTime = new Date(date);
startDateTime.setHours(startHour, startMinute);
const endDateTime = new Date(date);
endDateTime.setHours(endHour, endMinute);
const top = $(startRow).find('td[data-th="TOP"]').text().trim();
const thema = $(startRow).find('td[data-th="Thema"] a.bt-top-collapser').text().trim();
const beschreibungElem = $(startRow).find('td[data-th="Thema"] p');
const beschreibung = beschreibungElem ? beschreibungElem.html().replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, "").trim() : "";
const urlElem = $(startRow).find('td[data-th="Thema"] div div div button');
const url = urlElem ? `https://bundestag.de${urlElem.attr("data-url")}` : "";
const statusElem = $(startRow).find('td[data-th="Status/ Abstimmung"] p');
const status = statusElem ? statusElem.html().replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, "").trim() : "";
const eventDescription = status ? `Status: ${status}\n\n${beschreibung}` : beschreibung;
const agendaItem = {
start: startDateTime.toISOString(),
end: endDateTime.toISOString(),
top: isNaN(Number(top)) ? top : `TOP ${top}`,
thema: thema,
beschreibung: eventDescription,
url: url,
status: status,
uid: generateUID(startDateTime, thema, top),
dtstamp: new Date().toISOString(),
};
agendaItems.push(agendaItem);
}
});
return agendaItems;
}
function foldLine(line) {
if (line.length <= 70) {
return line;
}
let result = "";
while (line.length > 70) {
result += line.substring(0, 70) + "\r\n ";
line = line.substring(70);
}
result += line;
return result;
}
function createIcal(agendaItems) {
const cal = [
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//hutt.io//api.hutt.io/bt-to//",
"CALSCALE:GREGORIAN",
"COLOR:#808080",
"X-APPLE-CALENDAR-COLOR:#808080",
foldLine(`X-WR-CALNAME:Tagesordnung Bundestag`),
foldLine(
`X-WR-CALDESC:Dieses iCal-Feed stellt die aktuelle Tagesordnung des Plenums des Deutschen Bundestages zur Verfügung. Es aktualisiert sich alle 15min selbst. Zwar ist der Sitzungsverlauf auch online unter bundestag.de/tagesordnung einsehbar, doch leider werden diese Daten nicht in einem maschinenlesbaren Format zur Verfügung gestellt. Deshalb war es Zeit, das selbst in die Hand zu nehmen. Mehr Informationen über das Projekt: https://api.hutt.io/bt-to/.`,
),
foldLine(
`DESCRIPTION:Dieses iCal-Feed stellt die aktuelle Tagesordnung des Plenums des Deutschen Bundestages zur Verfügung. Es aktualisiert sich alle 15min selbst. Zwar ist der Sitzungsverlauf auch online unter bundestag.de/tagesordnung einsehbar, doch leider werden diese Daten nicht in einem maschinenlesbaren Format zur Verfügung gestellt. Deshalb war es Zeit, das selbst in die Hand zu nehmen. Mehr Informationen über das Projekt: https://api.hutt.io/bt-to/.`,
),
"SOURCE;VALUE=URI:https://api.hutt.io/bt-to/ical",
"BEGIN:VTIMEZONE",
"TZID:Europe/Berlin",
"BEGIN:STANDARD",
"TZNAME:CET",
"DTSTART:19701025T030000",
"TZOFFSETFROM:+0200",
"TZOFFSETTO:+0100",
"RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU",
"END:STANDARD",
"BEGIN:DAYLIGHT",
"TZNAME:CEST",
"DTSTART:19700329T020000",
"TZOFFSETFROM:+0100",
"TZOFFSETTO:+0200",
"RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU",
"END:DAYLIGHT",
"END:VTIMEZONE",
];
agendaItems.forEach((item) => {
let dtstart = new Date(item.start);
let dtend = new Date(item.end);
// Ensure dtend is at least one minute after dtstart
if (dtend <= dtstart) {
dtend = new Date(dtstart.getTime() + 60000); // Add one minute
}
cal.push("BEGIN:VEVENT");
cal.push(foldLine(`UID:${item.uid}`));
cal.push(foldLine(`DTSTAMP:${formatDate(item.dtstamp)}`));
cal.push(foldLine(`DTSTART:${formatDate(dtstart.toISOString())}`));
cal.push(foldLine(`DTEND:${formatDate(dtend.toISOString())}`));
cal.push(
foldLine(
`SUMMARY:${item.top ? `${item.top}: ${item.thema}` : item.thema}`,
),
);
cal.push(
foldLine(`DESCRIPTION:${item.beschreibung.replace(/\n/g, "\\n")}`),
);
if (item.url) {
cal.push(foldLine(`URL:${item.url}`));
}
cal.push("END:VEVENT");
});
cal.push("END:VCALENDAR"); // Ensure the END:VCALENDAR line is added
return cal.join("\r\n"); // Ensure CRLF line endings
}
function createXml(agendaItems) {
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n<agenda>\n';
agendaItems.forEach((item) => {
xml += " <event>\n";
xml += ` <start>${item.start}</start>\n`;
xml += ` <end>${item.end}</end>\n`;
xml += ` <top>${item.top}</top>\n`;
xml += ` <thema>${item.thema}</thema>\n`;
if (item.status) {
xml += ` <status>${item.status}</status>\n`;
}
xml += ` <beschreibung>${item.beschreibung}</beschreibung>\n`;
if (item.url) {
xml += ` <url>${item.url}</url>\n`;
}
xml += " </event>\n";
});
xml += "</agenda>";
console.log("Generated XML:", xml);
return xml;
}
function generateUID(startDateTime, thema, top) {
return `${startDateTime.getTime()}-${thema.replace(/\s+/g, "-").toLowerCase()}-${top.replace(/\s+/g, "-").toLowerCase()}@api.hutt.io`;
}
function formatDate(date) {
return date.replace(/[-:]/g, "").split(".")[0] + "Z";
}
function getWeekNumber(date) {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7));
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
const weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
return weekNo;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment