Botoscope®
SaaS qui transforme une boutique WooCommerce en bot Telegram avec catalogue, panier, paiement et commandes synchronisés avec la boutique.
- Lancé
- 2025
- Statut
- Fermé en 2026
- Plateforme
- Telegram
- Store
- WooCommerce
À propos
Botoscope fait fonctionner une boutique WooCommerce comme un bot Telegram. Le développement a commencé en 2024 ; le service a été lancé publiquement en 2025. Les clients parcourent les catégories, choisissent des variations, remplissent un panier, passent commande et paient dans la discussion, et la commande arrive dans WooCommerce. Le moteur décrit ici est le processus Node.js qui sert un bot. Il parle à Telegram via TDLib en mode bot, à la boutique via un proxy Botoscope, tient une copie MySQL du catalogue et conserve l'état de chaque discussion dans Redis.
L'essentiel de la difficulté vient de la discussion elle-même. Les ID de messages changent après l'envoi, les claviers de réponse ne peuvent pas être modifiés, chaque appui sur un bouton est une courte charge utile de callback, un texte saisi peut être une saisie comme un libellé de menu, et les anciens messages s'accumulent. Une grande partie du moteur existe pour qu'un flux de messages se comporte comme les écrans d'une application.
Le reste tient à la cohérence avec une boutique située sur un autre serveur : un import du catalogue qui ne s'exécute pas au démarrage, des mises à jour produit par produit poussées depuis WordPress, un stock et des prix conformes à la boutique, une seule commande WooCommerce par paiement qui suit les modifications du client, et un paiement par processeurs de cartes, passerelles crypto et Telegram Stars enregistré sur cette commande.
Botoscope · boutique dans Telegram
ce qui a été résolu
Moteur et modèle de processus
dans le produitCe que cela apporteChaque boutique a eu son propre bot, isolé des autres boutiques, qui a tourné pendant des semaines sur un petit serveur et a pris en compte les changements faits par le propriétaire dans la boutique.
- Problème
- Chaque boutique a besoin de son propre bot, avec sa propre session Telegram, son catalogue et ses réglages, à partir d'une seule base de code. Le processus doit tourner pendant des semaines sur de petits serveurs et accepter des commandes venant de la boutique.
- Solution
Une instance du moteur par bot : un client TDLib, une copie MySQL du catalogue et un état Redis, configurés par l'environnement et exécutés sous PM2 avec des limites de mémoire. Les modules communiquent par un bus d'événements et des registres de hooks ; les fonctionnalités payantes se chargent comme extensions ; la boutique atteint le bot par un petit point d'accès HTTP de commandes.
- Stack
Isoler les boutiques sans chemin de code multi-tenantUn processus par bot ; jeton, domaine de la boutique, clés et limites du forfait viennent de l'environnement
Il n'y a aucun ID de tenant nulle part dans le moteur. init() connecte un client bot TDLib, ouvre un pool knex et une connexion Redis, et lit le domaine de la boutique, la clé d'API du client, la limite de produits et l'indicateur de forfait gratuit dans des variables d'environnement. L'isolation entre boutiques relève du déploiement : chaque bot reçoit sa propre instance avec son propre répertoire de base TDLib et son propre schéma MySQL.
Cela garde les requêtes et les clés Redis simples (chat:<id>:<key>) au prix d'un processus par boutique. Les clés Redis ne portent aucun préfixe de bot ; deux instances ne doivent donc pas partager une même base Redis.
Faire tourner le bot sur TDLib plutôt que sur l'API Bot HTTPClient TDLib en mode bot : une connexion persistante, un flux de mises à jour pour les deux sens, des caches locaux
Le bot se connecte via TDLib (tdl avec tdl-tdlib-addon) avec un jeton de bot et garde une connexion de longue durée à Telegram. Les mises à jour sont poussées dans client.on('update'), de sorte que le moteur n'a besoin ni d'une URL de webhook joignable depuis Telegram ni d'une boucle d'interrogation getUpdates.
La raison principale est le modèle d'écran. TDLib signale les messages sortants du bot lui-même sous la forme updateNewMessage et updateMessageSendSucceeded, dans le même flux que les messages du client. Un seul gestionnaire enregistre chaque ID de message d'une discussion privée, envoyé ou reçu, et quand un écran change, le moteur les supprime avec deleteMessages (revoke: true). La discussion ne garde que l'écran actuel, et les saisies telles qu'une recherche ou une adresse disparaissent avec lui. Le même flux permet au moteur d'attendre qu'un album de photos soit réellement arrivé avant d'envoyer le panneau du produit.
TDLib couvre aussi des appels que l'API Bot HTTP n'a pas : getMessage relit le texte d'un message que le bot a envoyé plus tôt, ce qu'utilise le panneau des produits groupés pour redessiner ses boutons. getChat et getUser sont servis par le client TDLib avec sa base d'informations de discussion activée, et les médias partent depuis des chemins locaux ou par ID de fichier distant via la base de fichiers. La base de messages est désactivée parce que le bot ne garde pas d'historique, l'optimiseur de stockage élague les fichiers de TDLib, et TDLib fonctionne avec un seul thread de travail.
Le coût est que la sémantique de TDLib déborde dans l'application : les ID de messages sont provisoires jusqu'à ce que le serveur les confirme (voir la tâche sur la correspondance des ID), et le répertoire de session lie un bot à un seul processus.
Évolutions possibles que TDLib offre déjà : activer sa base de messages pour l'historique et la recherche par discussion, et envoyer des fichiers dépassant la limite d'envoi de 50 MB de l'API Bot HTTP. Le moteur ne fait ni l'un ni l'autre aujourd'hui ; il plafonne les documents à 50 MB.
De nombreux modules réagissent aux mêmes mises à jour et aux mêmes écransBus d'événements pour les mises à jour plus registres de hooks par discussion et globaux, ordonnés par priorité
Answer transforme les mises à jour TDLib en événements du bus : do_callback_query pour les appuis sur les boutons, do_command pour le texte, pre_checkout_query et successful_payment pour les paiements. Le panier, les favoris, la réservation, le support et le reste s'y abonnent et ne traitent que leurs propres actions, sans importer les gestionnaires des autres.
Pour la composition de l'interface, il y a deux registres de hooks. Hooks conserve des callbacks par chat_id (le menu contextuel courant, des boutons de produit supplémentaires), triés par priorité, et ajouter un hook avec une priorité existante le remplace. GlobalHooks fait de même pour toutes les discussions ; l'extension de support l'utilise pour ajouter un bouton à des panneaux de produits qui ne lui appartiennent pas. Les hooks par discussion sont supprimés quand la discussion est supprimée.
Activer et désactiver les fonctionnalités du forfait pour chaque boutiqueListe d'extensions synchronisée depuis la boutique ; classe construite à partir du slug ; redémarrage PM2 pour décharger
La liste des extensions actives (coupons, livraison, support, réservation, marketing, b2b et autres) est synchronisée dans la table des options. Extensions.init() convertit chaque slug en nom de classe et l'instancie avant que les modules du cœur soient construits, afin que leurs hooks existent d'abord. Les modules du cœur vérifient la présence d'une extension via des getters facultatifs et adaptent leur comportement ; par exemple, le paiement supprime l'étape du code promo quand les coupons sont désactivés.
Désactiver une extension ne peut pas décharger du code qui a déjà enregistré des écouteurs ; réinitialiser le cache des extensions redémarre donc le processus via PM2. Les commentaires du code notent que cela interrompt les utilisateurs en pleine session.
Laisser la boutique pousser des changements et des requêtes dans un bot en cours d'exécutionPoint d'accès HTTP /command ; commandes connues traitées directement, les autres acheminées vers les modules par le bus
Commander fait tourner un simple serveur http avec /health et /command. Les commandes connues (payment_success, order_is_done, reset_cache, update_product_cache) sont traitées directement. Tout le reste est émis comme exec_command ; le module qui en est responsable écrit is_executed et answer dans l'objet body partagé, que le point d'accès renvoie après une courte attente fixe.
reset_cache rafraîchit un cache de réglages ou tous, avec les extensions toujours en dernier car celui-là redémarre le processus. Le reset_db_cache() de chaque module recharge son blob JSON dans la table des options et vide sa copie en mémoire.
Garder en bonne santé un processus longue durée sur un petit serveurPM2 en mode fork, tas de 450 MB, redémarrage à 1 GB, 2 à 5 connexions BD, réinitialisations de cache planifiées
ecosystem.config.cjs exécute une seule instance en mode fork (le mode cluster est commenté ; un jeton de bot signifie une session TDLib), plafonne l'old space de V8, redémarre en cas de croissance de la mémoire et exige 30 s de disponibilité avant qu'un démarrage soit jugé stable. Les gestionnaires SIGTERM, SIGINT et SIGQUIT détruisent le pool knex avant la sortie, et un minuteur journalise l'usage du pool quand il y a de l'activité.
Les caches en mémoire, tels que les paniers et les devises sélectionnées, sont vidés par des minuteurs, et la table de correspondance des ID de messages est vidée à chaque changement d'écran.
La discussion comme interface d'application
dans le produitCe que cela apporteLes clients passaient du catalogue au panier puis au paiement comme entre les écrans d'une application, et la discussion restait propre au lieu de se remplir d'anciens messages.
- Problème
- Une discussion Telegram est un flux de messages, mais le produit a besoin d'écrans : une page de catalogue, un panier, une étape de paiement, chacun remplaçant le précédent, avec des boutons qui se mettent à jour sur place.
- Solution
Suivre chaque message par discussion, remplacer les écrans en supprimant l'ensemble précédent une fois le nouveau dessiné, faire correspondre les ID provisoires de TDLib aux ID du serveur pour les modifications ultérieures, et garder l'état de navigation dans un contexte par discussion dans Redis.
- Stack
Modifier un message dont l'ID change après l'envoiFaire correspondre les ID provisoires aux ID du serveur à partir de updateMessageSendSucceeded, par discussion en RAM
sendMessage dans TDLib renvoie un ID local provisoire ; le véritable ID arrive plus tard dans updateMessageSendSucceeded avec old_message_id. Le moteur stocke l'ID renvoyé par send() dans le contexte de la discussion (top_message_id, taxonomies_btn_mgs_id et similaires) et le résout par MessageMapping quand il doit modifier ou supprimer ce message.
La correspondance a d'abord vécu dans Redis, puis a été déplacée dans une table par discussion en mémoire, vidée à chaque changement d'écran pour rester petite. Sans elle, modifier un clavier en ligne après un clic viserait un ID qui n'existe plus.
Faire se comporter un groupe de messages comme un seul écranJournaliser les ID envoyés dans Redis ; sur un nouvel écran, les mettre de côté et les supprimer après le dessin
Chaque nouvel ID de message dans une discussion privée est ajouté à chat:<id>:messages. make_messages_castling() déplace cette liste vers chat:<id>:old_messages et réinitialise la correspondance des ID, le module dessine le nouvel écran, puis clear_display() supprime les anciens messages. Le nouveau contenu apparaît avant que l'ancien disparaisse, de sorte que la discussion n'est jamais vide entre deux écrans.
Un effacement complet de la discussion a été essayé et n'est resté dans le code que comme avertissement : sur certains téléphones, il éjectait l'utilisateur du bot.
Les messages à clavier de réponse ne peuvent pas être modifiésEnvoyer un nouveau message de tête avec le clavier mis à jour, puis supprimer l'ancien
Le menu persistant du bas affiche un bouton de panier avec un nombre d'articles en direct. Telegram n'autorise pas la modification du texte d'un message porteur d'un clavier de réponse ; Cart.redraw_menu() envoie donc un nouveau message de tête et supprime le précédent grâce à son ID mis en correspondance, tandis que le résumé du bas, un message en ligne, est modifié sur place après un court délai.
Le menu courant est conservé comme hook context_menu par discussion. change_menu_buttons_text() réécrit uniquement le bouton qui commence par l'émoji du panier dans le menu sérialisé, de sorte que n'importe quel écran peut rafraîchir le nombre d'articles du panier sans connaître sa propre disposition.
Les textes de la boutique sont en HTML ; TDLib veut du texte brut avec des décalages d'entitésConvertisseur maison : décoder les entités, retirer un sous-ensemble de balises, émettre des décalages textEntity
format_string_before_send() traite strong, em, u, s, code, pre et a href. Il trouve la balise ouvrante la plus proche, la retire avec sa balise fermante et note les positions de début et de fin dans la chaîne nettoyée ; elles deviennent les décalages textEntity dans formattedText. Les entités HTML nommées et numériques venant de WooCommerce sont décodées d'abord. Une balise mal formée ou non fermée fait renvoyer à la fonction le texte d'origine sans entités plutôt que d'envoyer un formatage cassé.
Deux astuces de mise en page l'accompagnent : une suite de caractères vides Braille remplit les messages pour que les fiches produit s'affichent à la même largeur, et un caractère de superposition combinant dessine un ancien prix barré dans les libellés de boutons, où les entités ne sont pas autorisées.
Un texte saisi peut être une saisie de l'utilisateur ou un bouton de menu localiséIndicateur par discussion pour la saisie attendue ; les libellés de boutons connus en sortent toujours
Les boutons du clavier de réponse arrivent comme du texte brut dans la langue de l'utilisateur ; l'aiguillage compare donc le texte entrant avec les libellés construits par get_menu_command_text() pour cette discussion. Quand une étape attend du texte libre (requête de recherche, SKU, adresse, code promo, message au support), le contexte contient is_waiting_user_input et la commande à reprendre. Answer enregistre le texte comme user_message_text et rejoue cette commande, sauf si le texte est une commande à barre oblique ou un libellé de navigation.
Les gestionnaires de saisie du paiement répètent la vérification avec les libellés de leur propre étape, de sorte qu'appuyer sur Retour pendant qu'une adresse est attendue fait revenir en arrière au lieu d'enregistrer ce mot comme adresse.
Les fiches produit doivent arriver dans l'ordre et résister à une pagination rapideAttendre la mise à jour propre à l'album avant son panneau ; autoriser une seule action de pagination à la fois
Une fiche produit est un album de médias suivi d'un panneau de texte avec des boutons. print_products_blocks() envoie l'album, attend jusqu'à 10 s l'événement new_message_coming de cette discussion, puis marque une pause avant d'envoyer le panneau, de sorte que le panneau n'atterrit jamais au-dessus de ses photos. De courtes pauses fixes avant chaque envoi et chaque suppression étalent les requêtes.
Des appuis rapides sur suivant et précédent démarreraient sinon un second remplacement d'écran avant que le premier ait supprimé ses messages. Un indicateur nowdo par discussion ignore la navigation pendant qu'un écran est remplacé et est libéré une seconde après la suppression des anciens messages.
Synchronisation du catalogue WooCommerce
dans le produitCe que cela apporteLes clients parcouraient le catalogue rapidement et voyaient les prix et le stock actuels. Les modifications faites dans la boutique arrivaient seules dans le bot, et les abonnés étaient prévenus du retour des articles.
- Problème
- La boutique tourne sur un site WordPress hébergé sur un autre serveur. Le bot doit parcourir rapidement le catalogue, afficher les prix et le stock actuels et prendre en compte les modifications sans interroger la boutique à chaque appui.
- Solution
Garder une copie MySQL des produits et des liens de catégories, remplie par un import complet paginé et mise à jour produit par produit quand la boutique pousse un changement. Les réglages de la boutique arrivent sous forme de blobs JSON dans une table d'options avec des caches en mémoire.
- Stack
Atteindre les données de chaque boutique sans détenir ses identifiants RESTTous les appels à la boutique passent par un proxy ; domaine, point d'accès, clé d'API et HMAC transitent dans les en-têtes
get_rest_data() et send_remote_data() envoient en POST vers une seule URL de proxy Botoscope. La boutique est nommée dans un en-tête client-domain, le point d'accès dans un autre en-tête, et la requête est authentifiée avec la clé d'API du client et un code dérivé par HMAC-SHA256. Les téléchargements de fichiers passent par le même proxy via son point d'accès fetch-file.
Le moteur n'a jamais besoin des clés REST de la boutique et le côté WordPress n'a qu'à répondre au proxy. Les échecs renvoient null et les appelants traitent null comme une absence de données ; une panne de la boutique se dégrade donc en listes vides au lieu de faire planter un écran.
Importer un catalogue complet à la demande, pas à chaque redémarrageVider la table, désactiver les index, récupérer des pages de 100, insérer par lots, réactiver les index dans finally
Products.reset_db_cache() lit le nombre de produits, vide products et products_vs_terms, exécute ALTER TABLE ... DISABLE KEYS sur les deux tables MyISAM et récupère products/{offset}/{limit} par pages de 100. Chaque page est insérée par lots de 100 lignes ; une erreur sur une page est journalisée et l'import passe à la page suivante. MyISAM n'a pas de transactions, de sorte que les lignes déjà écrites d'une page en échec restent. Les index sont réactivés dans un bloc finally, si bien qu'un import raté laisse tout de même des tables indexées. Pendant l'import, le bot affiche la partie du catalogue chargée jusque-là.
L'import ne s'exécute pas au démarrage. Un commentaire explique qu'avec un gros catalogue il pourrait bloquer le serveur après chaque redémarrage ; il ne s'exécute donc que sur une commande reset_cache explicite venant de la boutique.
Les charges utiles des produits varient selon le type et ne cessent de grossirDes colonnes pour ce qui est filtré ou trié, tout le reste dans une colonne JSON data
insert_product_batch() déstructure les champs que le moteur interroge (price, sale_price, indicateurs de stock, type, parent_id, publish_date, menu_order, minimum, maximum et pas du panier) en colonnes et sérialise le reste de la charge utile dans data. Les listes de médias, les téléchargements, les jours d'accès ou l'URL d'un produit externe peuvent être ajoutés côté boutique sans changement de schéma. Les ID de catégories vont à la fois dans une liste séparée par des virgules sur la ligne et dans products_vs_terms pour des recherches de catégories indexées.
install.sql s'exécute à chaque démarrage avec CREATE TABLE IF NOT EXISTS, de sorte qu'une nouvelle instance provisionne son propre schéma.
Appliquer des modifications de produit isolées sans réimport completLa boutique pousse update_product_cache ; produit, variations et termes réécrits à partir d'une seule réponse de la boutique
Quand un produit change dans WooCommerce, la boutique envoie update_product_cache à /command. update_product_cache() récupère products_filter/{id}, qui renvoie le produit avec ses variations, puis supprime la ligne du produit, les lignes ayant ce parent_id et ses liens de catégories, et insère les nouvelles lignes. Une seule requête porte le produit et ses variations, de sorte que les lignes sont réécrites une fois par changement.
La limite du forfait ne compte que les produits parents. L'import complet rogne les produits principaux par page et conserve toujours leurs variations ; le chemin incrémental ignore un nouveau produit dès que le nombre de produits distincts dans products_vs_terms atteint la limite.
Les indicateurs de stock dérivent et les clients veulent des alertes de retour en stockRéconcilier les indicateurs en stock à partir de la quantité toutes les 3 min ; notifier les abonnés toutes les 2 h
Pour les produits à stock géré, un minuteur toutes les trois minutes fixe is_in_stock d'après stock_quantity dans les deux sens, de sorte que le bouton d'achat suit la quantité réelle même si un indicateur poussé était périmé. Les clients peuvent s'abonner à un produit en rupture de stock ou à une seule variation ; stock_notify stocke product_id et chat_id. Toutes les deux heures, le moteur trouve les produits suivis qui sont de nouveau en stock, renvoie la fiche à chaque abonné et supprime l'abonnement.
Chaque requête de catalogue compare aussi publish_date à l'heure actuelle, de sorte que les produits planifiés restent masqués jusqu'à leur date.
Le filtrage à facettes ne tient pas dans des boutons de discussionOuvrir la page de filtre de la boutique comme Telegram Web App ; elle renvoie les ID correspondants
La commande /filter et les boutons Filtre ouvrent filter_page_url comme Web App avec le chat_id en suffixe. Le filtrage s'exécute côté WordPress. Le résultat revient au moteur sous la forme d'une commande filtered_products_ids sur /command, et Products affiche ces ID comme un catalogue paginé normal dans cette discussion.
Cela garde le comptage des facettes et la logique des attributs à un seul endroit, dans la boutique, tandis que la discussion affiche toujours les résultats dans son propre format de fiche.
Panier, variations et paiement
dans le produitCe que cela apporteLe bot suivait les règles de la boutique pour les quantités, les variations, les réservations et les remises, et la commande dans la boutique correspondait à ce que le client avait vu dans la discussion.
- Problème
- Le panier et le paiement doivent appliquer les règles de la boutique (pas de quantité, minimums, stock, variations, créneaux de réservation, remises) avec seulement des boutons et de courtes réponses textuelles, et la commande WooCommerce doit correspondre à ce que le client a vu.
- Solution
Les paniers vivent dans Redis derrière un cache RAM, les variations se choisissent avec un sélecteur en ligne en cascade, et le paiement est une machine à étapes élaguée selon le contenu du panier et les extensions activées. La commande est créée une fois puis modifiée au fil des changements du client.
- Stack
Le panier est lu à presque chaque affichageRedis par discussion derrière un cache RAM en écriture immédiate ; articles expirés et supprimés élagués
Cart.get() sert depuis une table statique par discussion et se rabat sur Redis (chat:<id>:cart) ; update() écrit dans les deux. Le libellé du bouton du panier, les totaux et chaque panneau de produit lisent le panier, de sorte que le cache économise à chaque fois un aller-retour Redis. Les articles peuvent porter une expiration, utilisée par les blocages de réservation, et sont supprimés à la lecture une fois expirés. Dessiner le panier retire d'abord les ID qui n'existent plus dans le catalogue local, de sorte qu'un produit supprimé dans WooCommerce ne peut pas arriver au paiement.
Les boutons de quantité respectent quantity_step, min_cart_count et le stock géré, et une variation sans valeurs propres hérite de celles du produit parent.
Choisir une variation avec des boutons quand toutes les combinaisons n'existent pasLignes d'attributs en cascade ; chaque ligne n'offre que les termes qui forment encore une vraie variation
get_buy_buttons_panel() lit la combinaison d'attributs de chaque variation et ordonne les attributs selon l'ordre des attributs de la boutique. La première ligne liste chaque terme utilisé ; chaque ligne suivante ne liste que les termes qui, avec les choix précédents, correspondent à au moins une variation existante. Un attribut de trois options ou plus peut s'afficher comme une ligne compacte précédent / courant / suivant plutôt que comme une grille.
Changer un attribut supérieur efface les inférieurs et se cale sur la première combinaison correspondante. Un produit s'ouvre sur sa variation la moins chère. Le choix est stocké par discussion et par produit dans le contexte, et le panneau est modifié sur place à chaque appui.
Les étapes du paiement dépendent du contenu du panierListe d'étapes élaguée par discussion : pas de livraison pour les paniers numériques, pas d'étape de paiement quand c'est gratuit
Les étapes de base sont le code promo, l'adresse de livraison, le mode de livraison, le mode de paiement, le commentaire et la confirmation. Les étapes des extensions désactivées sont retirées au démarrage. get_checkout_steps() inspecte ensuite le panier, en parcourant les produits groupés et les variations de leurs enfants variables pour trouver un article physique ; un panier sans article physique saute les deux étapes de livraison. Un total nul bascule vers la porte cadeau et saute le choix du paiement, une boutique avec une seule passerelle la présélectionne, et payer en Stars impose la passerelle Stars.
Les étapes obligatoires bloquent next() tant qu'une valeur n'est pas enregistrée, et toute étape peut être rouverte en mode édition depuis l'écran de confirmation et y revenir directement.
La commande de la boutique doit suivre les modifications faites après sa créationCréer la commande une fois à l'étape de paiement, garder son ID dans le contexte, pousser chaque modification ultérieure
generate_payment_button() n'appelle create_order que si le contexte n'a pas encore d'order_id, puis stocke l'ID. Si le client revient en arrière et change le mode de paiement, l'adresse, le code promo ou le commentaire, chaque gestionnaire appelle orders.edit(order_id, ...) pour que la commande WooCommerce soit modifiée au lieu d'être dupliquée.
La commande porte le chat_id Telegram et le nom du bot, de sorte que l'historique des commandes se récupère par discussion et qu'un lien « payer pour moi » peut retrouver la commande plus tard.
Deux clients du bot ne doivent pas réserver le même créneauBlocages en mémoire par créneau pendant 600 s, comptés par rapport à la capacité restante du créneau
Les créneaux viennent de la boutique par produit et par jour avec capacity et capacity_used, et les créneaux que la boutique marque comme réservés sont filtrés. Quand un client choisit un créneau, le moteur enregistre un blocage avec une expiration et ajoute la réservation au panier avec la même durée de blocage. is_freezed() compte les blocages actifs pour ce créneau, cette date et cette clé et bloque le créneau quand les blocages atteignent la capacité moins les places utilisées. Retirer l'article du panier libère son blocage via l'événement done_remove_from_cart.
Les dates du calendrier et le filtrage des créneaux passés utilisent le fuseau horaire de la boutique tiré des réglages, et non l'horloge du serveur. Les blocages vivent dans la mémoire du processus du bot, donc ils ne s'appliquent qu'aux clients de ce bot et sont réinitialisés au redémarrage ; les réservations faites au même moment sur le site web ne sont pas bloquées.
Les promotions comme « un article sur 3 à -25 % » ont besoin d'un format de règlesPetit langage de formules : conditions sur le nombre d'articles ; remise ; n-ième, le moins cher ou le plus cher, plafond
Les campagnes marketing utilisent des formules telles que 'c >= 6 & c < 12; 25%; 3n, me, cap=3'. parse_formula() les découpe en conditions sur le nombre d'articles, une remise en pourcentage ou fixe, et des modificateurs (une unité sur n, la moins chère ou la plus chère d'abord, un plafond). get_applied_amount() développe le panier en une entrée par unité de produits de la campagne, applique les formules dans l'ordre et marque les unités utilisées pour qu'une formule ultérieure ne puisse pas les remiser à nouveau.
Les fenêtres de campagne sont vérifiées dans le fuseau horaire de la boutique, les campagnes en mode test ne s'appliquent qu'à la discussion de l'administrateur, et la remise de campagne est ignorée quand un coupon s'applique déjà. Les coupons limités à des produits s'étendent aux enfants des produits groupés et variables.
Paiements et devises
dans le produitCe que cela apporteLes clients voyaient les prix dans leur propre devise et payaient par carte, en crypto ou avec Telegram Stars, ou demandaient à un ami de payer. La boutique recevait une commande payée.
- Problème
- Le paiement commence dans la discussion, se termine sur une page de passerelle ou dans Telegram, et doit aboutir à une commande WooCommerce payée, à travers plusieurs passerelles et devises d'affichage.
- Solution
Les montants restent dans la devise de base de la boutique et ne sont convertis que pour l'affichage et l'encaissement. Le moteur crée lui-même les sessions de passerelle et interroge régulièrement les passerelles qui n'ont pas de webhook pour le bot.
- Stack
Prix affichés dans une devise, encaissés dans une autreMontants gardés dans la devise de base, convertis à l'affichage et à l'encaissement, commandes normalisées en retour
Les totaux du panier, les remises et la livraison sont calculés dans la devise de base. format_price() convertit vers la devise choisie pour la discussion avec le taux synchronisé et applique la position du symbole et les décimales, en arrondissant au-dessus quand les centimes sont masqués. generate_payment_button() recompte le total dans la devise choisie avant d'appeler une passerelle. Les commandes récupérées de WooCommerce dans une autre devise sont divisées par leur taux de commande enregistré, de sorte que l'historique et les totaux correspondent à l'arithmétique du bot.
Quand la boutique n'autorise pas le paiement dans d'autres devises, la sélection est réinitialisée sur la devise de base à l'étape de paiement et le client est informé de la raison.
Accepter Telegram Stars comme mode de paiementStars comme devise de boutique XTR avec un taux ; facture TDLib dont la charge utile nomme la commande
Les Stars sont configurées comme une devise nommée XTR dans la liste des devises de la boutique, de sorte que leur taux arrive par la même synchronisation. Le montant est le total de base multiplié par le taux XTR, arrondi au-dessus. La facture est envoyée en inputMessageInvoice avec un jeton de fournisseur vide et une charge utile JSON en base64 contenant order_id, chat_id, amount et gateway. La requête de pré-paiement reçoit une réponse, et sur messagePaymentSuccessfulBot la charge utile est décodée et la boutique est informée que la commande est payée.
Sans devise XTR, l'option Stars signale une erreur au lieu d'émettre une facture.
Passerelle crypto sans webhook enregistré pour le botInterrogation par discussion toutes les 8 s pendant 15 min au plus, annulée quand le panier est rouvert
Pour CryptoBot, le moteur crée une facture libellée en monnaie fiduciaire avec une expiration de 900 s et interroge getInvoices toutes les 8 s. Une Map de minuteurs indexée par chat_id garde une seule interrogation active par client, et l'ouverture du panier l'annule. Une fois payée, la commande passe en traitement et le panier est vidé ; si la facture a expiré ou été annulée, le client reçoit un message.
CoinGate fonctionne dans l'autre sens : son URL de callback pointe vers le proxy Botoscope, qui transmet le webhook à /command sous la forme payment_success, et la commande est finalisée quand le webhook signale paid.
Chaque passerelle de cartes a besoin de son propre format de sessionSessions Stripe et LiqPay construites dans le moteur, ouvertes comme Web App ou URL ; PayPal en sandbox
Chaque passerelle a son propre constructeur : Stripe via une Checkout Session, LiqPay via un blob de données en base64 signé en SHA-1 sur la clé privée, les données et la clé privée. Le lien est envoyé comme bouton Web App ou URL, éventuellement précédé de boutons de politique de remboursement, de confidentialité et de livraison. Le constructeur PayPal (identifiants client OAuth, puis un paiement avec des URL de retour et d'annulation) envoie le paiement au point d'accès sandbox de PayPal dans ce code, il ne fonctionne donc que pour les tests.
Les modes hors ligne (coordonnées de carte bancaire, SWIFT, cadeau, payer-pour-moi) affichent des instructions traduites et un bouton de confirmation au lieu d'un lien.
Laisser un autre utilisateur Telegram payer une commandeUn lien profond payer-pour-moi ouvre la commande dans la discussion du payeur et prévient l'acheteur
Choisir payer-pour-moi termine le paiement par un lien t.me qui démarre le bot avec payforme_<order_id>. Quand un autre utilisateur l'ouvre, le moteur charge la commande depuis la boutique, applique la devise de la commande, retire les modes hors ligne et payer-pour-moi et affiche les passerelles restantes pour le total de la commande. Après le paiement, la discussion de l'acheteur reçoit un message nommant le payeur.
Médias, fichiers et langues
dans le produitCe que cela apporteLes photos et vidéos des produits s'ouvraient vite, les fichiers achetés arrivaient directement dans la discussion, et les clients lisaient le menu et le catalogue dans leur propre langue.
- Problème
- Les images et fichiers des produits se trouvent dans la boutique, et les envoyer via Telegram à chaque affichage est lent. Le bot télécharge aussi des URL qui viennent des données de la boutique. L'interface et le catalogue doivent se lire dans la langue du client.
- Solution
Un cache local de médias indexé par hachage, des téléchargements contrôlés via le proxy, la réutilisation de fichiers déjà hébergés par Telegram, et des traductions en couches : chaînes d'interface fournies, remplacements du propriétaire et traductions par champ dans les données du catalogue.
- Stack
Retélécharger les photos de produit à chaque affichage de ficheCache local indexé par MD5 de l'URL, réparti en dossiers de deux caractères hexadécimaux, envoyé comme fichiers locaux
send_media_gallery() hache chaque URL de média, enregistre le fichier sous media_files_cache/<first two hex chars>/<hash> à la première utilisation, puis donne ensuite à TDLib le chemin local. Les fichiers de zéro octet sont supprimés et ignorés, un album est plafonné à 8 éléments, et une variation sans images propres se rabat sur les médias du produit parent.
Les liens vers des publications t.me contournent le cache et vont à TDLib comme fichiers distants.
Le bot télécharge des URL tirées des données de la boutiqueListe blanche de domaines, extensions bloquées, plafonds de taille par type, fichiers partiels supprimés
downloadFileToLocal() n'accepte que les URL http(s) sur le domaine du client ou les hôtes Telegram, rejette les extensions exécutables et de scripts dans les liens directs, envoie une requête HEAD pour rejeter les fichiers au-dessus du plafond (10 MB pour les images et l'audio, 50 MB pour la vidéo et les documents), puis diffuse le fichier via le point d'accès fetch-file du proxy avec le même plafond. Les téléchargements vides, trop gros ou en échec sont supprimés du disque.
Les grosses vidéos de produits sont lentes à envoyer depuis la boutiquePublier la vidéo dans un canal ; le bot répond avec son ID de fichier Telegram pour la réutiliser
Answer surveille les publications du canal. Quand une vidéo apparaît dans un canal dont le bot est membre, le bot répond avec l'ID de fichier distant de la vidéo. Un lien de publication t.me ou cet ID peut alors servir de média produit, et le moteur l'envoie comme fichier distant, de sorte que Telegram le sert depuis son propre stockage, sans envoi par client.
Livrer des fichiers numériques payants dans la discussionAprès le paiement, envoyer chaque téléchargement via la récupération contrôlée, mis en cache sous un nom masqué
Quand le panier contenait des produits téléchargeables et que la commande est payée en ligne ou gratuite, do_after_pay() envoie chaque téléchargement de ces produits comme document. Les fichiers sont mis en cache sous un nom dérivé de l'ID du téléchargement par shiftUUID(), de sorte que le nom stocké n'est pas l'ID de la boutique, et les documents de plus de 50 MB sont refusés. Les produits à accès limité affichent les jours restants, calculés à partir de la date de paiement de la commande.
Les factures PDF du point d'accès de factures de la boutique suivent le même chemin.
Texte d'interface en 7 langues, modifiable par le propriétaire de la boutiqueJSON fourni par langue, recouvert par les remplacements du propriétaire synchronisés, avec repli sur l'anglais
Sept fichiers de langue (en, es, pt, ru, pl, kz, ua) avec les mêmes 215 clés se chargent au démarrage. __() fusionne le fichier de langue de la discussion avec les remplacements que le propriétaire a saisis dans l'administration (synchronisés sous interface_translations et indexés comme s<number>) ; si une clé manque encore, il réessaie avec l'anglais plus les remplacements. La langue choisie est stockée par discussion ; au premier contact, elle vient du language_code du client Telegram quand cette langue est activée, sinon de la valeur par défaut de la boutique.
Comme les boutons du menu sont reconnus par leur texte localisé, les libellés d'une discussion sont résolus en parallèle avec Promise.all à chaque contrôle d'aiguillage.
Le contenu du catalogue et la recherche doivent fonctionner dans chaque langueTraductions par champ sur les produits et les termes ; la recherche porte sur le titre ou les traductions
Les produits, catégories, marques, champs méta, modes de livraison et de paiement portent un objet translations. translate_to() renvoie 0 quand la discussion utilise la langue par défaut, de sorte que le cas courant saute toute recherche ; sinon get_field_translation() choisit le champ traduit et se rabat sur l'original. La recherche par titre exécute un LIKE à la fois sur la colonne du titre et sur le JSON des traductions stockées, avec un minimum de 3 caractères, de sorte qu'une requête dans n'importe quelle langue active trouve le produit.
Les dates dans les méta des produits et les périodes d'accès sont formatées avec la langue de la discussion comme paramètre régional.
Chiffres
du produit- Langues du bot 7 Anglais, espagnol, portugais, russe, polonais, kazakh, ukrainien.
- Modes de paiement en ligne 5 Stripe, LiqPay, CoinGate, CryptoBot et Telegram Stars.
- Chemin de réponse push, sans interrogation TDLib garde une connexion persistante avec Telegram : chaque appui atteint le bot immédiatement, sans détour par un webhook et sans délai d'interrogation.