Sprache

Kontakt aufnehmen

SaaS · seit 2025

Botoscope®

SaaS, das einen WooCommerce-Shop in einen Telegram-Bot verwandelt, mit Katalog, Warenkorb, Kasse und mit dem Shop synchronisierten Bestellungen.

Gestartet
2025
Status
Eingestellt 2026
Plattform
Telegram
Store
WooCommerce
  • Node.js (ES modules)
  • TDLib via tdl / tdl-tdlib-addon
  • MySQL / MariaDB (knex, mysql2)
  • Redis (ioredis)
  • PM2
  • axios
  • moment
  • Telegram Web Apps
  • WooCommerce via Botoscope proxy
botoscope.com öffnen botoscope.com

Über mich

Botoscope betreibt einen WooCommerce-Shop als Telegram-Bot. Die Entwicklung begann 2024; der Dienst ging 2025 öffentlich an den Start. Kunden durchsuchen Kategorien, wählen Variationen, füllen einen Warenkorb, gehen zur Kasse und bezahlen im Chat, und die Bestellung landet in WooCommerce. Die hier beschriebene Engine ist der Node.js-Prozess, der einen Bot bedient. Er spricht mit Telegram über TDLib im Bot-Modus, mit dem Shop über einen Botoscope-Proxy, hält eine MySQL-Kopie des Katalogs und führt den Zustand pro Chat in Redis.

Der größte Teil der Schwierigkeit kommt vom Chat selbst. Nachrichten-IDs ändern sich nach dem Senden, Reply-Keyboards lassen sich nicht bearbeiten, jeder Tastendruck ist eine kurze Callback-Nutzlast, getippter Text kann Eingabe oder Menübeschriftung sein, und alte Nachrichten häufen sich an. Ein großer Teil der Engine existiert, damit ein Strom von Nachrichten sich wie die Bildschirme einer App verhält.

Der Rest ist Konsistenz mit einem Shop auf einem anderen Server: ein Katalogimport, der nicht beim Start läuft, Einzelprodukt-Updates, die von WordPress gepusht werden, Lagerbestand und Preise, die mit dem Shop übereinstimmen, eine WooCommerce-Bestellung pro Kasse, die den Änderungen des Kunden folgt, und Zahlungen über Kartenprozessoren, Krypto-Gateways und Telegram Stars, die in dieser Bestellung erfasst werden.

Botoscope · Shop in Telegram

was gelöst wurde

EINGESTELLTseit 2025

Engine und Prozessmodell

im Produkt

Was es bringtJeder Shop bekam seinen eigenen, von anderen Shops getrennten Bot, der wochenlang auf einem kleinen Server lief und Änderungen des Inhabers im Shop übernahm.

Problem
Jeder Shop braucht seinen eigenen Bot mit eigener Telegram-Sitzung, eigenem Katalog und eigenen Einstellungen, aus einer Codebasis. Der Prozess muss auf kleinen Servern wochenlang laufen und Befehle von der Shop-Seite annehmen.
Lösung

Eine Engine-Instanz pro Bot: ein TDLib-Client, eine MySQL-Katalogkopie und Redis-Zustand, per Umgebung konfiguriert und unter PM2 mit Speicherlimits betrieben. Module sprechen über einen Event-Bus und Hook-Registries; kostenpflichtige Funktionen werden als Erweiterungen geladen; die Shop-Seite erreicht den Bot über einen kleinen HTTP-Command-Endpunkt.

Stack
  • Node.js
  • TDLib (tdl)
  • EventEmitter
  • PM2
  • knex
  • ioredis
Aufgaben · 6
Shops isolieren, ohne einen Multi-Tenant-CodepfadEin Prozess pro Bot; Token, Shop-Domain, Schlüssel und Tarifgrenzen kommen aus der Umgebung

Es gibt nirgends in der Engine eine Tenant-ID. init() meldet einen TDLib-Bot-Client an, öffnet einen knex-Pool und eine Redis-Verbindung und liest Shop-Domain, Client-API-Schlüssel, Produktlimit und das Flag für den kostenlosen Tarif aus Umgebungsvariablen. Die Isolation zwischen Shops ist eine Frage des Deployments: Jeder Bot erhält eine eigene Instanz mit eigenem TDLib-Datenbankverzeichnis und eigenem MySQL-Schema.

Das hält Abfragen und Redis-Keys einfach (chat:<id>:<key>), auf Kosten eines Prozesses pro Shop. Redis-Keys tragen kein Bot-Präfix, daher dürfen sich zwei Instanzen keine Redis-Datenbank teilen.

Den Bot auf TDLib statt auf der HTTP Bot API betreibenTDLib-Client im Bot-Modus: eine dauerhafte Verbindung, ein Update-Strom für beide Richtungen, lokale Caches

Der Bot meldet sich über TDLib (tdl mit tdl-tdlib-addon) mit einem Bot-Token an und hält eine langlebige Verbindung zu Telegram. Updates werden in client.on('update') geschoben, sodass die Engine weder eine von Telegram erreichbare Webhook-URL noch eine getUpdates-Polling-Schleife braucht.

Der Hauptgrund ist das Bildschirmmodell. TDLib meldet die eigenen ausgehenden Nachrichten des Bots als updateNewMessage und updateMessageSendSucceeded im selben Strom wie die Nachrichten des Kunden. Ein Handler registriert jede Nachrichten-ID in einem privaten Chat, gesendet oder empfangen, und wenn sich ein Bildschirm ändert, löscht die Engine sie mit deleteMessages (revoke: true). Der Chat behält nur den aktuellen Bildschirm, und getippte Eingaben wie eine Suchanfrage oder eine Adresse verschwinden mit ihm. Derselbe Strom lässt die Engine warten, bis ein Fotoalbum tatsächlich angekommen ist, bevor sie das Produktpanel sendet.

TDLib deckt auch Aufrufe ab, die die HTTP Bot API nicht hat: getMessage liest den Text einer früher vom Bot gesendeten Nachricht zurück, was das Panel gruppierter Produkte nutzt, um seine Buttons neu zu zeichnen. getChat und getUser beantwortet der TDLib-Client mit eingeschalteter Chat-Info-Datenbank, und Medien gehen von lokalen Pfaden oder per Remote-Datei-ID über die Dateidatenbank raus. Die Nachrichtendatenbank ist aus, weil der Bot keinen Verlauf führt, der Speicheroptimierer bereinigt die Dateien von TDLib, und TDLib läuft mit einem Worker-Thread.

Der Preis ist, dass die Semantik von TDLib in die Anwendung durchschlägt: Nachrichten-IDs sind vorläufig, bis der Server sie bestätigt (siehe die Aufgabe zur ID-Zuordnung), und das Sitzungsverzeichnis bindet einen Bot an einen Prozess.

Mögliche nächste Schritte, die TDLib bereits bietet: die Nachrichtendatenbank für Verlauf und Suche pro Chat einzuschalten und Dateien zu senden, die größer sind als das Upload-Limit von 50 MB der HTTP Bot API. Die Engine tut heute keines von beidem; sie begrenzt Dokumente auf 50 MB.

Viele Module reagieren auf dieselben Updates und BildschirmeEvent-Bus für Updates plus Hook-Registries pro Chat und global, nach Priorität geordnet

Answer macht aus TDLib-Updates Bus-Events: do_callback_query für Tastendrücke, do_command für Text, pre_checkout_query und successful_payment für Zahlungen. Warenkorb, Favoriten, Buchung, Support und der Rest abonnieren und behandeln nur ihre eigenen Aktionen, sodass sie die Handler der anderen nicht importieren.

Für die Komposition der Oberfläche gibt es zwei Hook-Registries. Hooks hält Callbacks pro chat_id (das aktuelle Kontextmenü, zusätzliche Produkt-Buttons), nach Priorität sortiert, wobei das Hinzufügen eines Hooks mit bestehender Priorität diesen ersetzt. GlobalHooks tut dasselbe für alle Chats; die Support-Erweiterung nutzt es, um Produktpanels, die ihr nicht gehören, einen Button hinzuzufügen. Hooks pro Chat werden verworfen, wenn der Chat gelöscht wird.

Tarifmerkmale pro Shop ein- und ausschaltenErweiterungsliste vom Shop synchronisiert; Klasse aus dem Slug gebaut; PM2-Neustart zum Entladen

Die Liste aktiver Erweiterungen (Gutscheine, Versand, Support, Buchung, Marketing, B2B und weitere) wird in die Options-Tabelle synchronisiert. Extensions.init() wandelt jeden Slug in einen Klassennamen um und instanziiert ihn, bevor die Kernmodule gebaut werden, sodass deren Hooks zuerst existieren. Kernmodule prüfen eine Erweiterung über optionale Getter und ändern ihr Verhalten; zum Beispiel lässt die Kasse den Schritt für den Promo-Code weg, wenn Gutscheine aus sind.

Das Deaktivieren einer Erweiterung kann Code, der bereits Listener registriert hat, nicht entladen, daher startet das Zurücksetzen des Erweiterungs-Caches den Prozess über PM2 neu. Die Code-Kommentare merken an, dass dies Benutzer mitten in einer Sitzung unterbricht.

Die Shop-Seite Änderungen und Abfragen in einen laufenden Bot schieben lassenHTTP-Endpunkt /command; bekannte Befehle direkt behandelt, der Rest über den Bus an Module geleitet

Commander betreibt einen einfachen http-Server mit /health und /command. Bekannte Befehle (payment_success, order_is_done, reset_cache, update_product_cache) werden direkt behandelt. Alles andere wird als exec_command ausgelöst; das zuständige Modul schreibt is_executed und answer in das gemeinsame Body-Objekt, das der Endpunkt nach einer kurzen festen Wartezeit zurückgibt.

reset_cache aktualisiert einen Einstellungs-Cache oder alle, wobei die Erweiterungen immer zuletzt kommen, weil dieser den Prozess neu startet. Das reset_db_cache() jedes Moduls holt seinen JSON-Blob neu in die Options-Tabelle und leert seine Kopie im Speicher.

Einen langlaufenden Prozess auf einem kleinen Server gesund haltenPM2-Fork-Modus, 450 MB Heap, Neustart bei 1 GB, 2 bis 5 DB-Verbindungen, zeitgesteuerte Cache-Resets

ecosystem.config.cjs betreibt eine einzelne Fork-Instanz (der Cluster-Modus ist auskommentiert; ein Bot-Token bedeutet eine TDLib-Sitzung), begrenzt den V8-Old-Space, startet bei Speicherwachstum neu und verlangt 30 s Laufzeit, bevor ein Start als stabil zählt. Handler für SIGTERM, SIGINT und SIGQUIT zerstören den knex-Pool vor dem Beenden, und ein Timer protokolliert die Pool-Nutzung, wenn Aktivität besteht.

Caches im Speicher wie Warenkörbe und gewählte Währungen werden per Timer geleert, und die Map der Nachrichten-IDs wird bei jedem Bildschirmwechsel geleert.

Chat als Anwendungsoberfläche

im Produkt

Was es bringtKunden bewegten sich durch Katalog, Warenkorb und Kasse wie durch Bildschirme einer App, und der Chat blieb aufgeräumt, statt sich mit alten Nachrichten zu füllen.

Problem
Ein Telegram-Chat ist ein Strom von Nachrichten, aber das Produkt braucht Bildschirme: eine Katalogseite, einen Warenkorb, einen Kassenschritt, die jeweils den vorherigen ersetzen, mit Buttons, die sich an Ort und Stelle aktualisieren.
Lösung

Jede Nachricht pro Chat verfolgen, Bildschirme tauschen, indem der vorherige Satz nach dem Zeichnen des neuen gelöscht wird, die vorläufigen IDs von TDLib auf Server-IDs für spätere Änderungen abbilden und den Navigationszustand in einem Kontext pro Chat in Redis halten.

Stack
  • TDLib
  • Redis
  • Telegram reply and inline keyboards
Aufgaben · 6
Eine Nachricht bearbeiten, deren ID sich nach dem Senden ändertVorläufige IDs auf Server-IDs aus updateMessageSendSucceeded abbilden, pro Chat im RAM

sendMessage in TDLib gibt eine vorläufige lokale ID zurück; die echte ID trifft später in updateMessageSendSucceeded zusammen mit old_message_id ein. Die Engine speichert die von send() zurückgegebene ID im Chat-Kontext (top_message_id, taxonomies_btn_mgs_id und ähnliche) und löst sie über MessageMapping auf, wenn sie diese Nachricht bearbeiten oder löschen muss.

Die Zuordnung lag zuerst in Redis und wurde in eine Map pro Chat im Speicher verlegt, die bei jedem Bildschirmwechsel geleert wird, damit sie klein bleibt. Ohne sie würde das Bearbeiten einer Inline-Tastatur nach einem Klick eine ID treffen, die nicht mehr existiert.

Eine Gruppe von Nachrichten wie einen Bildschirm verhalten lassenGesendete IDs in Redis protokollieren; bei einem neuen Bildschirm beiseiteschieben und nach dem Zeichnen löschen

Jede neue Nachrichten-ID in einem privaten Chat wird nach chat:<id>:messages geschoben. make_messages_castling() verschiebt diese Liste nach chat:<id>:old_messages und setzt die ID-Zuordnung zurück, das Modul zeichnet den neuen Bildschirm, und clear_display() löscht dann die alten Nachrichten. Der neue Inhalt erscheint, bevor der alte verschwindet, sodass der Chat zwischen Bildschirmen nie leer wird.

Ein vollständiges Leeren des Chats wurde ausprobiert und nur als Warnung im Code belassen: Auf manchen Telefonen warf es den Benutzer aus dem Bot.

Nachrichten mit Reply-Keyboard lassen sich nicht bearbeitenEine neue oberste Nachricht mit der aktualisierten Tastatur senden, dann die alte löschen

Das dauerhafte untere Menü zeigt einen Warenkorb-Button mit einer Live-Artikelanzahl. Telegram erlaubt nicht, den Text einer Nachricht zu bearbeiten, die ein Reply-Keyboard trägt, daher sendet Cart.redraw_menu() eine frische oberste Nachricht und löscht die vorherige über ihre zugeordnete ID, während die untere Zusammenfassung, eine Inline-Nachricht, nach einer kurzen Verzögerung an Ort und Stelle bearbeitet wird.

Das aktuelle Menü wird als Hook context_menu pro Chat gehalten. change_menu_buttons_text() schreibt im serialisierten Menü nur den Button um, der mit dem Warenkorb-Emoji beginnt, sodass jeder Bildschirm die Warenkorbanzahl aktualisieren kann, ohne sein eigenes Layout zu kennen.

Shop-Texte sind HTML; TDLib will Klartext mit Entity-OffsetsEigener Konverter: Entities dekodieren, eine Teilmenge von Tags entfernen, textEntity-Offsets ausgeben

format_string_before_send() behandelt strong, em, u, s, code, pre und a href. Es findet das nächste öffnende Tag, entfernt es samt schließendem Tag und hält Start- und Endposition im bereinigten String fest; daraus werden die textEntity-Offsets in formattedText. Benannte und numerische HTML-Entities aus WooCommerce werden zuerst dekodiert. Ein fehlerhaftes oder nicht geschlossenes Tag lässt die Funktion den Originaltext ohne Entities zurückgeben, statt kaputte Formatierung zu senden.

Daneben stehen zwei Layout-Tricks: Eine Folge von Braille-Leerzeichen füllt Nachrichten auf, damit Produktkarten in derselben Breite gerendert werden, und ein kombinierendes Überlagerungszeichen zeichnet einen durchgestrichenen alten Preis in Button-Beschriftungen, wo Entities nicht erlaubt sind.

Getippter Text kann Benutzereingabe oder eine lokalisierte Menü-Schaltfläche seinFlag pro Chat für erwartete Eingabe; bekannte Button-Beschriftungen brechen immer daraus aus

Reply-Keyboard-Buttons kommen als einfacher Text in der Sprache des Benutzers an, daher vergleicht das Routing den eingehenden Text mit Beschriftungen, die get_menu_command_text() für diesen Chat baut. Wartet ein Schritt auf freien Text (Suchanfrage, SKU, Adresse, Promo-Code, Support-Nachricht), hält der Kontext is_waiting_user_input und den Befehl zum Fortsetzen. Answer speichert den Text als user_message_text und spielt diesen Befehl erneut ab, es sei denn, der Text ist ein Slash-Befehl oder eine Navigationsbeschriftung.

Eingabe-Handler der Kasse wiederholen die Prüfung gegen die Beschriftungen ihres eigenen Schritts, sodass „Zurück“ bei erwarteter Adresse zurücknavigiert, statt das Wort als Adresse zu speichern.

Produktkarten müssen in der richtigen Reihenfolge ankommen und schnelles Blättern überstehenAuf das eigene Update des Albums warten, bevor sein Panel kommt; nur eine Blätteraktion zugleich zulassen

Eine Produktkarte ist ein Medienalbum, gefolgt von einem Textpanel mit Buttons. print_products_blocks() sendet das Album, wartet bis zu 10 s auf das Event new_message_coming dieses Chats und pausiert dann vor dem Senden des Panels, damit das Panel nie über seinen Fotos landet. Kurze feste Pausen vor jedem Senden und Löschen verteilen die Requests.

Schnelle Taps auf „weiter“ und „zurück“ würden sonst einen zweiten Bildschirmwechsel starten, bevor der erste seine Nachrichten gelöscht hat. Ein Flag nowdo pro Chat ignoriert Navigation, während ein Bildschirm ersetzt wird, und wird eine Sekunde nach dem Löschen der alten Nachrichten freigegeben.

Synchronisierung des WooCommerce-Katalogs

im Produkt

Was es bringtKunden blätterten den Katalog schnell und sahen aktuelle Preise und Lagerbestand. Änderungen im Shop erreichten den Bot von selbst, und Abonnenten erfuhren, wann Artikel wieder verfügbar waren.

Problem
Der Shop läuft auf einer WordPress-Website auf einem anderen Server. Der Bot muss den Katalog schnell blättern, aktuelle Preise und Lagerbestand zeigen und Änderungen übernehmen, ohne bei jedem Tap den Shop abzufragen.
Lösung

Eine MySQL-Kopie von Produkten und Kategorieverknüpfungen halten, befüllt durch einen paginierten Vollimport und pro Produkt aktualisiert, wenn der Shop eine Änderung pusht. Shop-Einstellungen kommen als JSON-Blobs in einer Options-Tabelle mit Caches im Speicher an.

Stack
  • MySQL (MyISAM)
  • knex
  • WooCommerce REST via proxy
  • Telegram Web Apps
Aufgaben · 6
Die Daten jedes Shops erreichen, ohne seine REST-Zugangsdaten zu haltenAlle Shop-Aufrufe gehen an einen Proxy; Domain, Endpunkt, API-Schlüssel und HMAC laufen in Headern mit

get_rest_data() und send_remote_data() posten an eine einzige Botoscope-Proxy-URL. Der Shop wird in einem client-domain-Header genannt, der Endpunkt in einem anderen Header, und der Request wird mit dem Client-API-Schlüssel und einem mit HMAC-SHA256 abgeleiteten Pass authentifiziert. Datei-Downloads nutzen denselben Proxy über seinen fetch-file-Endpunkt.

Die Engine braucht nie die eigenen REST-Schlüssel des Shops, und die WordPress-Seite muss nur dem Proxy antworten. Fehler geben null zurück, und Aufrufer behandeln null als keine Daten, sodass ein Ausfall des Shops zu leeren Listen degradiert, statt einen Bildschirm abstürzen zu lassen.

Einen vollständigen Katalog auf Anforderung importieren, nicht bei jedem NeustartTruncate, Keys deaktivieren, Seiten zu 100 holen, in Batches einfügen, Keys in finally wieder aktivieren

Products.reset_db_cache() liest die Produktanzahl, leert products und products_vs_terms per Truncate, führt ALTER TABLE ... DISABLE KEYS auf beiden MyISAM-Tabellen aus und holt products/{offset}/{limit} in Seiten zu 100. Jede Seite wird in Batches zu 100 Zeilen eingefügt; ein Fehler auf einer Seite wird protokolliert, und der Import geht zur nächsten Seite über. MyISAM kennt keine Transaktionen, daher bleiben bereits geschriebene Zeilen einer fehlgeschlagenen Seite bestehen. Die Keys werden in einem finally-Block wieder aktiviert, sodass auch ein fehlgeschlagener Import indizierte Tabellen hinterlässt. Während der Import läuft, zeigt der Bot den bisher geladenen Teil des Katalogs.

Der Import läuft nicht beim Start. Ein Kommentar erklärt, dass er bei einem großen Katalog den Server nach jedem Neustart ausbremsen könnte, daher läuft er nur bei einem ausdrücklichen Befehl reset_cache vom Shop.

Produkt-Nutzdaten unterscheiden sich je Typ und wachsen weiterSpalten für das, was gefiltert oder sortiert wird, alles Übrige in einer JSON-Spalte data

insert_product_batch() zerlegt die Felder, die die Engine abfragt (price, sale_price, Lager-Flags, type, parent_id, publish_date, menu_order, Warenkorb-Minimum, -Maximum und -Schrittweite), in Spalten und serialisiert den Rest der Nutzdaten nach data. Medienlisten, Downloads, Zugriffstage oder eine externe Produkt-URL lassen sich auf der Shop-Seite ohne Schemaänderung ergänzen. Kategorie-IDs gehen sowohl in eine kommagetrennte Liste in der Zeile als auch in products_vs_terms für indizierte Kategorieabfragen.

install.sql läuft bei jedem Start mit CREATE TABLE IF NOT EXISTS, sodass eine neue Instanz ihr eigenes Schema einrichtet.

Änderungen an einzelnen Produkten ohne vollständigen Reimport übernehmenDer Shop pusht update_product_cache; Produkt, Variationen und Terms werden aus einer Shop-Antwort neu geschrieben

Ändert sich ein Produkt in WooCommerce, sendet die Shop-Seite update_product_cache an /command. update_product_cache() holt products_filter/{id}, das das Produkt mit seinen Variationen zurückgibt, löscht dann die Produktzeile, Zeilen mit diesem parent_id und seine Kategorieverknüpfungen und fügt die neuen Zeilen ein. Ein Request trägt das Produkt und seine Variationen, sodass die Zeilen pro Änderung einmal neu geschrieben werden.

Die Tarifgrenze zählt nur Elternprodukte. Der Vollimport kürzt die Hauptprodukte pro Seite und behält ihre Variationen immer; der inkrementelle Pfad überspringt ein neues Produkt, sobald die Anzahl verschiedener Produkte in products_vs_terms das Limit erreicht.

Lager-Flags driften, und Kunden wollen Benachrichtigungen bei WiederverfügbarkeitIn-Stock-Flags alle 3 Min. aus der Menge abgleichen; Abonnenten alle 2 h benachrichtigen

Bei Produkten mit verwaltetem Lagerbestand setzt ein Timer alle drei Minuten is_in_stock aus stock_quantity in beide Richtungen, sodass der Kaufen-Button der tatsächlichen Menge folgt, auch wenn ein gepushtes Flag veraltet war. Kunden können ein nicht vorrätiges Produkt oder eine einzelne Variation abonnieren; stock_notify speichert product_id und chat_id. Alle zwei Stunden findet die Engine abonnierte Produkte, die wieder vorrätig sind, sendet jedem Abonnenten die Karte erneut und entfernt das Abonnement.

Jede Katalogabfrage vergleicht außerdem publish_date mit der aktuellen Zeit, sodass geplante Produkte bis zu ihrem Datum verborgen bleiben.

Facettenfilterung passt nicht in Chat-ButtonsDie Filterseite des Shops als Telegram Web App öffnen; sie sendet passende IDs zurück

Der Befehl /filter und die Filter-Buttons öffnen filter_page_url als Web App mit angehängter chat_id. Das Filtern läuft auf der WordPress-Seite. Das Ergebnis kehrt als Befehl filtered_products_ids auf /command zur Engine zurück, und Products gibt diese IDs in diesem Chat als normalen paginierten Katalog aus.

Das hält Facettenzählung und Attributlogik an einer Stelle, im Shop, während der Chat die Ergebnisse weiterhin in seinem eigenen Kartenformat zeigt.

Warenkorb, Variationen und Kasse

im Produkt

Was es bringtDer Bot folgte den eigenen Regeln des Shops für Mengen, Variationen, Buchungen und Rabatte, und die Bestellung im Shop entsprach dem, was der Kunde im Chat gesehen hatte.

Problem
Warenkorb und Kasse müssen Shop-Regeln (Mengenschritte, Mindestmengen, Lagerbestand, Variationen, Buchungsslots, Rabatte) nur mit Buttons und kurzen Textantworten durchsetzen, und die WooCommerce-Bestellung muss dem entsprechen, was der Kunde gesehen hat.
Lösung

Warenkörbe liegen in Redis hinter einem RAM-Cache, Variationen werden mit einer kaskadierenden Inline-Auswahl gewählt, und die Kasse ist eine Schrittmaschine, die nach Warenkorbinhalt und aktivierten Erweiterungen beschnitten wird. Die Bestellung wird einmal angelegt und bei Änderungen des Kunden angepasst.

Stack
  • Redis
  • Node.js
  • moment
  • WooCommerce orders via proxy
Aufgaben · 6
Der Warenkorb wird bei fast jedem Rendern gelesenRedis pro Chat hinter einem Write-through-RAM-Cache; abgelaufene und gelöschte Artikel bereinigt

Cart.get() liefert aus einer statischen Map pro Chat und fällt auf Redis zurück (chat:<id>:cart); update() schreibt in beide. Die Beschriftung des Warenkorb-Buttons, die Summen und jedes Produktpanel lesen den Warenkorb, daher spart der Cache jedes Mal einen Redis-Round-Trip. Artikel können ein Ablaufdatum tragen, das Buchungsreservierungen nutzen, und werden beim Lesen verworfen, sobald sie abgelaufen sind. Beim Zeichnen des Warenkorbs werden zuerst IDs entfernt, die im lokalen Katalog nicht mehr existieren, sodass ein in WooCommerce gelöschtes Produkt nicht an die Kasse gelangen kann.

Mengen-Buttons beachten quantity_step, min_cart_count und verwalteten Lagerbestand, und eine Variation ohne eigene Werte erbt sie vom Elternprodukt.

Eine Variation per Buttons wählen, wenn nicht jede Kombination existiertKaskadierende Attributzeilen; jede Zeile bietet nur Terms an, die noch eine echte Variation ergeben

get_buy_buttons_panel() liest die Attributkombination jeder Variation und ordnet Attribute nach der Attributreihenfolge des Shops. Die erste Zeile listet jeden verwendeten Term; jede folgende Zeile listet nur Terms, die zusammen mit den obigen Auswahlen mindestens eine vorhandene Variation treffen. Ein Attribut mit drei oder mehr Optionen kann statt als Raster als kompakte Zeile vorherige / aktuelle / nächste dargestellt werden.

Das Ändern eines oberen Attributs leert die unteren und rastet auf der ersten passenden Kombination ein. Ein Produkt öffnet sich mit seiner günstigsten Variation. Die Wahl wird pro Chat und Produkt im Kontext gespeichert, und das Panel wird bei jedem Tap an Ort und Stelle bearbeitet.

Kassenschritte hängen davon ab, was im Warenkorb liegtSchrittliste pro Chat beschnitten: kein Versand bei digitalen Warenkörben, kein Zahlungsschritt bei kostenlosen

Die Basisschritte sind Promo-Code, Lieferadresse, Versandart, Zahlungsart, Kommentar und Bestätigung. Schritte deaktivierter Erweiterungen werden beim Start entfernt. get_checkout_steps() untersucht dann den Warenkorb, geht gruppierte Produkte und die Variationen ihrer variablen Kinder durch, um einen physischen Artikel zu finden; ein Warenkorb ohne einen solchen überspringt beide Versandschritte. Eine Gesamtsumme von null schaltet auf das Geschenk-Gate und überspringt die Zahlungsartwahl, ein Shop mit einem Gateway wählt es vor, und Bezahlen mit Stars erzwingt das Stars-Gate.

Pflichtschritte blockieren next(), bis ein Wert gespeichert ist, und jeder Schritt kann vom Bestätigungsbildschirm im Bearbeitungsmodus erneut geöffnet werden und von dort direkt dorthin zurückkehren.

Die Shop-Bestellung muss Änderungen folgen, die nach ihrer Anlage erfolgenDie Bestellung einmal im Zahlungsschritt anlegen, ihre ID im Kontext halten, jede spätere Änderung nachschieben

generate_payment_button() ruft create_order nur auf, wenn der Kontext noch keine order_id hat, und speichert dann die ID. Geht der Kunde zurück und ändert Zahlungsart, Adresse, Promo-Code oder Kommentar, ruft jeder Handler orders.edit(order_id, ...) auf, sodass die WooCommerce-Bestellung angepasst statt dupliziert wird.

Die Bestellung trägt die Telegram-chat_id und den Bot-Namen, sodass der Bestellverlauf pro Chat geholt wird und ein Pay-for-me-Link die Bestellung später finden kann.

Zwei Bot-Kunden dürfen nicht denselben Zeitslot buchenReservierungen im Speicher pro Slot für 600 s, gegen die verbleibende Kapazität des Slots gezählt

Slots kommen vom Shop pro Produkt und Tag mit capacity und capacity_used, und Slots, die der Shop als reserviert markiert, werden herausgefiltert. Wählt ein Kunde einen Slot, legt die Engine eine Reservierung mit Ablaufzeit an und fügt die Buchung dem Warenkorb mit derselben Haltezeit hinzu. is_freezed() zählt aktive Reservierungen für diesen Slot, dieses Datum und diesen Key und sperrt den Slot, sobald die Reservierungen die Kapazität minus belegter Plätze erreichen. Das Entfernen des Artikels aus dem Warenkorb gibt seine Reservierung über das Event done_remove_from_cart frei.

Kalenderdaten und die Filterung vergangener Slots verwenden die Shop-Zeitzone aus den Einstellungen, nicht die Serveruhr. Reservierungen leben im Prozessspeicher des Bots, gelten also nur für Kunden dieses Bots und werden bei einem Neustart zurückgesetzt; Buchungen, die zur selben Zeit auf der Website erfolgen, werden nicht reserviert.

Aktionen wie „jeder 3. Artikel 25 % günstiger“ brauchen ein RegelformatKleine Formelsprache: Artikelanzahl-Bedingungen; Rabatt; n-ter, günstigster oder teuerster, Obergrenze

Marketingkampagnen verwenden Formeln wie 'c >= 6 & c < 12; 25%; 3n, me, cap=3'. parse_formula() zerlegt sie in Artikelanzahl-Bedingungen, einen prozentualen oder festen Rabatt und Modifikatoren (jede n-te Einheit, günstigste oder teuerste zuerst, eine Obergrenze). get_applied_amount() expandiert den Warenkorb zu einem Eintrag pro Einheit der Kampagnenprodukte, wendet die Formeln der Reihe nach an und markiert verbrauchte Einheiten, damit eine spätere Formel sie nicht erneut rabattieren kann.

Kampagnenzeiträume werden in der Shop-Zeitzone geprüft, Kampagnen im Testmodus gelten nur für den Admin-Chat, und der Kampagnenrabatt entfällt, wenn bereits ein Gutschein gilt. Gutscheine, die auf Produkte beschränkt sind, erstrecken sich auf die Kinder gruppierter und variabler Produkte.

Zahlungen und Währungen

im Produkt

Was es bringtKunden sahen Preise in ihrer eigenen Währung und zahlten per Karte, Krypto oder Telegram Stars oder baten einen Freund zu zahlen. Der Shop erhielt eine bezahlte Bestellung.

Problem
Die Zahlung beginnt im Chat, endet auf einer Gateway-Seite oder innerhalb von Telegram und muss als bezahlte WooCommerce-Bestellung enden, über mehrere Gateways und Anzeigewährungen hinweg.
Lösung

Beträge bleiben in der Basiswährung des Shops und werden nur für Anzeige und Abbuchung umgerechnet. Die Engine erstellt Gateway-Sitzungen selbst und fragt Gateways ab, die keinen Webhook für den Bot haben.

Stack
  • PayPal (sandbox)
  • Stripe Checkout
  • LiqPay
  • CoinGate
  • CryptoBot
  • Telegram Stars (TDLib invoices)
Aufgaben · 5
Preise in einer Währung angezeigt, in einer anderen abgebuchtBeträge in der Basiswährung gehalten, bei Anzeige und Abbuchung umgerechnet, Bestellungen zurücknormalisiert

Warenkorbsummen, Rabatte und Versand werden in der Basiswährung berechnet. format_price() rechnet mit dem synchronisierten Kurs in die gewählte Währung des Chats um und wendet Position des Symbols und Dezimalstellen an, wobei aufgerundet wird, wenn Cent ausgeblendet sind. generate_payment_button() rechnet die Summe vor dem Aufruf eines Gateways in die gewählte Währung um. Aus WooCommerce geholte Bestellungen in einer anderen Währung werden durch ihren gespeicherten Bestellkurs geteilt, sodass Verlauf und Summen mit der eigenen Arithmetik des Bots übereinstimmen.

Erlaubt der Shop keine Zahlung in anderen Währungen, wird die Auswahl im Zahlungsschritt auf die Basiswährung zurückgesetzt und der Kunde erfährt den Grund.

Telegram Stars als Zahlungsart akzeptierenStars als XTR-Shopwährung mit einem Kurs; TDLib-Invoice, deren Payload die Bestellung benennt

Stars sind als Währung namens XTR in der Währungsliste des Shops konfiguriert, sodass ihr Kurs über dieselbe Synchronisierung eintrifft. Der Betrag ist die Basissumme mal XTR-Kurs, aufgerundet. Die Invoice wird als inputMessageInvoice mit leerem Provider-Token und einer Base64-JSON-Payload gesendet, die order_id, chat_id, amount und gateway enthält. Die Pre-Checkout-Abfrage wird beantwortet, und bei messagePaymentSuccessfulBot wird die Payload dekodiert und der Shop benachrichtigt, dass die Bestellung bezahlt ist.

Ohne XTR-Währung meldet die Stars-Option einen Fehler statt einer Invoice.

Krypto-Gateway ohne für den Bot registrierten WebhookPolling pro Chat alle 8 s bis zu 15 Min., abgebrochen, wenn der Warenkorb erneut geöffnet wird

Für CryptoBot erstellt die Engine eine auf Fiat lautende Invoice mit 900 s Ablaufzeit und fragt getInvoices alle 8 s ab. Eine Map von Timern, nach chat_id geschlüsselt, hält pro Kunde ein aktives Polling, und das Öffnen des Warenkorbs bricht es ab. Bei „paid“ wird die Bestellung auf „processing“ gesetzt und der Warenkorb geleert; bei „expired“ oder „cancelled“ erhält der Kunde eine Nachricht.

CoinGate geht den anderen Weg: Seine Callback-URL zeigt auf den Botoscope-Proxy, der den Webhook als payment_success an /command weiterleitet, und die Bestellung wird abgeschlossen, wenn der Webhook „paid“ meldet.

Kartengateways brauchen jeweils ihr eigenes SitzungsformatStripe- und LiqPay-Sitzungen in der Engine gebaut, als Web App oder URL geöffnet; PayPal in der Sandbox

Jedes Gateway hat seinen eigenen Builder: Stripe über eine Checkout Session, LiqPay über einen Base64-Datenblob, signiert mit SHA-1 über privaten Schlüssel, Daten und privaten Schlüssel. Der Link wird als Web-App- oder URL-Button gesendet, wahlweise mit vorangestellten Buttons für Rückerstattungs-, Datenschutz- und Versandbedingungen. Der PayPal-Builder (OAuth Client Credentials, dann eine Zahlung mit Return- und Cancel-URLs) postet die Zahlung in diesem Code an den PayPal-Sandbox-Endpunkt, funktioniert also nur zum Testen.

Offline-Methoden (Bankkartendaten, SWIFT, Geschenk, Pay-for-me) zeigen übersetzte Anweisungen und einen Bestätigungs-Button statt eines Links.

Einen anderen Telegram-Benutzer eine Bestellung bezahlen lassenPay-for-me-Deep-Link öffnet die Bestellung im Chat des Zahlers und benachrichtigt den Käufer

Die Wahl von Pay-for-me beendet die Kasse mit einem t.me-Link, der den Bot mit payforme_<order_id> startet. Öffnet ihn ein anderer Benutzer, lädt die Engine die Bestellung aus dem Shop, wendet die Währung der Bestellung an, entfernt Offline- und Pay-for-me-Methoden und zeigt die übrigen Gateways für die Bestellsumme. Nach der Zahlung erhält der Chat des Käufers eine Nachricht, die den Zahler nennt.

Medien, Dateien und Sprachen

im Produkt

Was es bringtProduktfotos und -videos öffneten sich schnell, gekaufte Dateien kamen direkt im Chat an, und Kunden lasen Menü und Katalog in ihrer eigenen Sprache.

Problem
Produktbilder und Dateien liegen im Shop, und sie bei jeder Ansicht über Telegram zu senden ist langsam. Der Bot lädt außerdem URLs herunter, die aus Shop-Daten stammen. Oberfläche und Katalog müssen in der Sprache des Kunden zu lesen sein.
Lösung

Ein gehashter lokaler Medien-Cache, abgesicherte Downloads über den Proxy, Wiederverwendung von Dateien, die Telegram bereits hostet, und geschichtete Übersetzungen: mitgelieferte Oberflächentexte, Überschreibungen des Inhabers und Übersetzungen pro Feld in den Katalogdaten.

Stack
  • TDLib file API
  • Node streams
  • crypto (MD5, HMAC)
  • JSON language packs
Aufgaben · 6
Produktfotos bei jeder Kartenansicht neu herunterladenLokaler Cache mit dem MD5 der URL als Key, in Ordner aus zwei Hex-Zeichen aufgeteilt, als lokale Dateien gesendet

send_media_gallery() hasht jede Medien-URL, speichert die Datei bei der ersten Nutzung unter media_files_cache/<erste zwei Hex-Zeichen>/<hash> und übergibt TDLib danach den lokalen Pfad. Dateien mit null Byte werden gelöscht und übersprungen, ein Album ist auf 8 Elemente begrenzt, und eine Variation ohne eigene Bilder greift auf die Medien des Elternprodukts zurück.

Links auf t.me-Beiträge umgehen den Cache und gehen als Remote-Dateien an TDLib.

Der Bot lädt URLs herunter, die aus Shop-Daten stammenDomain-Allowlist, gesperrte Endungen, Größenlimits pro Typ, Teildateien entfernt

downloadFileToLocal() akzeptiert nur http(s)-URLs auf der eigenen Domain des Clients oder auf Telegram-Hosts, lehnt ausführbare und Skript-Endungen in direkten Links ab, sendet eine HEAD-Anfrage, um Dateien über dem Limit abzulehnen (10 MB für Bilder und Audio, 50 MB für Video und Dokumente), und streamt die Datei dann mit demselben Limit über den fetch-file-Endpunkt des Proxys. Leere, zu große und fehlgeschlagene Downloads werden von der Platte gelöscht.

Große Produktvideos sind vom Shop aus langsam zu sendenDas Video in einem Kanal posten; der Bot antwortet mit seiner Telegram-Datei-ID zur Wiederverwendung

Answer beobachtet Kanalbeiträge. Erscheint ein Video in einem Kanal, dem der Bot angehört, antwortet der Bot mit der Remote-Datei-ID des Videos. Ein t.me-Beitragslink oder diese ID kann dann als Produktmedium dienen, und die Engine sendet es als Remote-Datei, sodass Telegram es aus seinem eigenen Speicher ausliefert, ohne Upload pro Kunde.

Bezahlte digitale Dateien im Chat ausliefernNach der Zahlung jeden Download über den abgesicherten Abruf senden, unter einem maskierten Namen gecacht

Enthielt der Warenkorb herunterladbare Produkte und ist die Bestellung online bezahlt oder kostenlos, sendet do_after_pay() jeden Download dieser Produkte als Dokument. Dateien werden unter einem Namen gecacht, der von shiftUUID() aus der Download-ID abgeleitet wird, sodass der gespeicherte Name nicht die ID des Shops ist, und Dokumente über 50 MB werden abgelehnt. Produkte mit begrenztem Zugriff zeigen die verbleibenden Tage, berechnet ab dem Zahlungsdatum der Bestellung.

PDF-Rechnungen vom Rechnungs-Endpunkt des Shops laufen über denselben Pfad.

Oberflächentexte in 7 Sprachen, vom Shop-Inhaber bearbeitbarMitgeliefertes JSON pro Sprache, überlagert von synchronisierten Überschreibungen des Inhabers, mit Rückfall auf Englisch

Sieben Sprachdateien (en, es, pt, ru, pl, kz, ua) mit denselben 215 Keys werden beim Start geladen. __() führt die Sprachdatei des Chats mit Überschreibungen zusammen, die der Inhaber im Admin eingegeben hat (synchronisiert als interface_translations und indiziert als s<number>); fehlt ein Key weiterhin, versucht es erneut mit Englisch plus Überschreibungen. Die gewählte Sprache wird pro Chat gespeichert; beim ersten Kontakt stammt sie aus dem language_code des Telegram-Clients, wenn diese Sprache aktiviert ist, sonst aus dem Shop-Standard.

Weil Menü-Buttons über ihren lokalisierten Text zugeordnet werden, werden die Beschriftungen für einen Chat bei jeder Routing-Prüfung parallel mit Promise.all aufgelöst.

Katalogeinhalte und Suche müssen in jeder Sprache funktionierenÜbersetzungen pro Feld bei Produkten und Terms; die Suche trifft Titel oder Übersetzungen

Produkte, Kategorien, Marken, Meta-Felder, Versand- und Zahlungsarten tragen ein translations-Objekt. translate_to() gibt 0 zurück, wenn der Chat die Standardsprache nutzt, sodass der Normalfall jede Nachschlagung überspringt; andernfalls wählt get_field_translation() das übersetzte Feld und fällt auf das Original zurück. Die Titelsuche führt LIKE sowohl über die Titelspalte als auch über das gespeicherte translations-JSON aus, mit einem Minimum von 3 Zeichen, sodass eine Anfrage in jeder aktiven Sprache das Produkt findet.

Daten in Produkt-Meta und Zugriffszeiträume werden mit der Sprache des Chats als Locale formatiert.

Zahlen

aus dem Produkt
  • Bot-Sprachen 7 Englisch, Spanisch, Portugiesisch, Russisch, Polnisch, Kasachisch, Ukrainisch.
  • Online-Zahlungsarten 5 Stripe, LiqPay, CoinGate, CryptoBot und Telegram Stars.
  • Antwortweg Push, kein Polling TDLib hält eine dauerhafte Verbindung zu Telegram: Jeder Tap erreicht den Bot sofort, ohne Webhook-Umweg und ohne Polling-Verzögerung.