PullMD Teil 3: Die API, und wie aus Reddit jede URL wurde
Am Ende von Teil 2 lief ein Container. Was man da eigentlich aufrufen kann, was zurückkommt und warum es so aussieht, wie es aussieht: darum geht es hier.
Und darum, dass sich das Ganze zwei Tage nach dem ersten Deploy schon wieder selbst überholt hat.
Ein Endpunkt, mehr nicht
Die Aufgabenbeschreibung für den Server war schon ziemlich genau. Sie steht so in der Session vom 2. März:
GET /apimiturl,comments,comment_depth,comment_limitundformat- Antwort als
text/markdown; charset=utf-8 - bei
format=textwird das Markdown rausgeputzt und es kommttext/plain - Fehler als JSON, mit 400 für eine fehlende oder ungültige URL, 404 wenn es den Beitrag nicht gibt, 502 wenn Reddit dichtmacht, 500 für den Rest
GET /shareleitet mit 302 auf die Startseite weiter, mit der URL im Fragment
Mehr war es nicht. Ein Endpunkt, ein Umleiter, dazu die statischen Dateien der Oberfläche.
Zwei Kleinigkeiten daran finde ich im Rückblick wichtiger, als sie damals aussahen.
Die erste sind die Fehlercodes. Ein 502 heißt hier ausdrücklich „Reddit hat dichtgemacht“, nicht „bei mir ist was kaputt“. Wer den Dienst automatisiert aufruft, kann an der Zahl schon ablesen, ob ein neuer Versuch überhaupt Sinn hat. Bei einer 400 hilft Wiederholen ja nicht.
Die zweite steht gar nicht in der Liste: createApp() nimmt optional eine eigene extractPost-Funktion entgegen. Damit lässt sich der Server testen, ohne dass irgendwer Reddit anfassen muss.
Das klingt nach nichts. Es ist aber der Unterschied zwischen Tests, die in Millisekunden laufen, und Tests, die man irgendwann abschaltet, weil sie nerven.
Und format=text ist die Sorte Funktion, die man einbaut, weil man sie selbst braucht. Manchmal will ich einfach nur den Fließtext, ohne Sternchen und Rauten. Dafür putzt der Dienst das Markdown wieder raus, das er gerade erzeugt hat. Ein bisschen albern, im Alltag aber genau richtig.
Die Reddit-API war erstmal keine Option
Eine Sache hatte ich schon im Konzept-Chat entschieden, und zwar bevor eine Zeile Code existierte.
„Also API fällt aus, die bekommt man nur noch auf Anfrage. Ja, wenn das mit JSON funktioniert, prima. Ansonsten einen normalen Browser imitieren?“
Die offizielle Reddit-API hätte eine Registrierung gebraucht, Schlüssel, ein Kontingent. Für ein Werkzeug, das ich mir selbst auf den Server stelle, ist das ziemlich viel Aufwand für ziemlich wenig.
Der .json-Weg kostet dagegen nichts und funktioniert ohne Konto. Wenn er blockiert wird, imitiert der Dienst halt einen normalen Browser und liest die alte Reddit-Oberfläche. Das ist unelegant, aber es hält.
Im selben Absatz steht übrigens noch ein Halbsatz, den ich danach fast zwei Monate lang vergessen habe: „MCP später.“ Dazu kommen wir in Teil 7.
Zwei Tage später: jede Adresse
Für Reddit lief das Ding jetzt ganz gut. Nur landet man im Alltag ja nicht nur auf Reddit.
Manchmal will man einfach irgendeine Seite mit dem Chat teilen: einen Blogartikel, eine Doku-Seite, was einem so über den Weg läuft. Ein großer Plan steckte da nicht dahinter, es lag einfach nahe. Wenn das Ding schon eine Seite in Markdown verwandelt, warum dann nur eine einzige Sorte von Seiten?
Am 4. März stand der nächste Durchgang an, und der hat aus dem Reddit-Werkzeug etwas anderes gemacht.
Sieben Aufgaben, gut eine halbe Stunde:
- Vier neue Pakete: Mozilla Readability, linkedom, Turndown, better-sqlite3
lib/cache.js, ein SQLite-Cache mit Historie, testgetrieben gebautlib/web.js, Web zu Markdown, mit Cloudflare-Erkennung und Readability plus Turndown als Rückfallebeneserver.jsentscheidet jetzt, ob eine Adresse zu Reddit gehört oder nicht- Die Oberfläche nimmt beliebige Adressen an und bekommt eine Historie
- Manifest und Service Worker auf v2
- Docker so umbauen, dass better-sqlite3 durchkompiliert
Seitdem gibt es drei Wege durch den Dienst, und daran hat sich bis heute nichts geändert:
-
Erst wird nachgesehen, ob eine Seite von sich aus Markdown liefert. Cloudflare kann das für Seiten, die es aktiviert haben, und dann ist die ganze eigene Aufbereitung ohnehin überflüssig.
-
Ist es Reddit, geht es weiter über den
.json-Weg von oben. -
Alles andere landet bei Readability und Turndown.
Der Cache aus Aufgabe 2 ist übrigens so ein Punkt, von dem ich damals nicht gedacht hätte, dass ich ihn später noch so oft brauche. Jede Abfrage landet in einer SQLite-Datei. Damit gibt es zum ersten Mal eine Historie: Was habe ich eigentlich schon alles durch das Ding geschickt? Aus dieser Tabelle wird später der Share-Link, und noch später die Statistik, die dafür sorgen soll, dass mir ein Fehler nicht noch einmal so lange entgeht.
Das Deploy, mit zwei Stolpersteinen
Der Umzug auf den Docker-Host lief am selben Vormittag. 168 Kilobyte per rsync, dann neu bauen.
Der Build musste better-sqlite3 nativ übersetzen, was auf einem Alpine-Image erst mal eine Werkzeugkette braucht. Also python3, make und g++ installieren, kompilieren, und die drei danach wieder rauswerfen, damit das Image klein bleibt.
Der erste Start ging trotzdem schief:
SQLITE_CANTOPEN
Das Datenverzeichnis gehörte dem Benutzer auf dem Host, der Container läuft aber als ein anderer. Ein chmod später lief es.
Der zweite Stolperstein war kurioser. Wikipedia kam als sauberes Markdown zurück, Hacker News auch. example.com dagegen nicht:
Failed to extract page: fetch failed
Schuld war ein Stammzertifikat, das Node 20 nicht mehr mitbringt. Ausgerechnet die Seite, die man zum Testen nimmt, weil sie immer geht.
Was im Kopf des Markdowns stand
Der Dienst schreibt über jede Ausgabe ein paar Zeilen mit Titel und Herkunft. Nach dem Umbau sah das so aus:
# Markdown - Wikipedia
**en.wikipedia.org**
From Wikipedia, the free encyclopedia
...
Nur die Domain also, nicht die vollständige Adresse. Bei Reddit stand sie schon drin, bei normalen Seiten eben nicht.
Das ist eine dieser Kleinigkeiten, die einem erst auffallen, wenn man das Werkzeug ein paar Wochen benutzt hat. Was daraus wurde, steht in Teil 5.
Teil 3 von 15 der PullMD Serie.
← PullMD Teil 2: Vom Spec zum laufenden Container | PullMD Teil 4: Der Skill, und was er wirklich bringt →