Idioma

Contactar

SaaS · desde 2025

Botoscope®

SaaS que convierte una tienda WooCommerce en un bot de Telegram con catálogo, carrito, pago y pedidos sincronizados con la tienda.

Lanzado
2025
Estado
Cerrado en 2026
Plataforma
Telegram
Tienda
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
Abrir botoscope.com botoscope.com

Sobre mí

Botoscope gestiona una tienda WooCommerce como un bot de Telegram. El desarrollo comenzó en 2024; el servicio se lanzó públicamente en 2025. Los clientes exploran categorías, eligen variaciones, llenan un carrito, pagan y abonan dentro del chat, y el pedido llega a WooCommerce. El motor descrito aquí es el proceso Node.js que atiende a un bot. Habla con Telegram mediante TDLib en modo bot, con la tienda a través de un proxy de Botoscope, mantiene una copia MySQL del catálogo y guarda el estado por chat en Redis.

Gran parte de la dificultad viene del propio chat. Los ID de los mensajes cambian tras el envío, los teclados de respuesta no se pueden editar, cada pulsación de botón es una breve carga de callback, el texto escrito puede ser una entrada o la etiqueta de un menú, y los mensajes antiguos se acumulan. Una parte importante del motor existe para que un flujo de mensajes se comporte como las pantallas de una aplicación.

El resto es la coherencia con una tienda alojada en otro servidor: una importación del catálogo que no se ejecuta al arrancar, actualizaciones por producto enviadas desde WordPress, stock y precios que coinciden con los de la tienda, un pedido de WooCommerce por pago que sigue las ediciones del cliente, y el pago mediante procesadores de tarjetas, pasarelas de criptomonedas y Telegram Stars registrado en ese pedido.

Botoscope · tienda en Telegram

qué se resolvió

CERRADOdesde 2025

Motor y modelo de procesos

en el producto

Qué aportaCada tienda tuvo su propio bot, aislado de las demás tiendas, que funcionó durante semanas en un servidor pequeño y recogió los cambios que el propietario hacía en la tienda.

Problema
Cada tienda necesita su propio bot con su propia sesión de Telegram, catálogo y ajustes, desde una única base de código. El proceso tiene que funcionar durante semanas en servidores pequeños y aceptar órdenes desde el lado de la tienda.
Solución

Una instancia del motor por bot: un cliente TDLib, una copia MySQL del catálogo y estado en Redis, configurados por entorno y ejecutados bajo PM2 con límites de memoria. Los módulos se comunican mediante un bus de eventos y registros de hooks; las funciones de pago se cargan como extensiones; el lado de la tienda llega al bot mediante un pequeño endpoint HTTP de comandos.

Stack
  • Node.js
  • TDLib (tdl)
  • EventEmitter
  • PM2
  • knex
  • ioredis
Tareas · 6
Aislar las tiendas sin una ruta de código multiinquilinoUn proceso por bot; el token, el dominio de la tienda, las claves y los límites del plan vienen del entorno

No hay ningún ID de inquilino en el motor. init() inicia sesión con un cliente de bot TDLib, abre un pool de knex y una conexión Redis, y lee el dominio de la tienda, la clave de API del cliente, el límite de productos y el indicador de plan gratuito de las variables de entorno. El aislamiento entre tiendas es una cuestión de despliegue: cada bot tiene su propia instancia con su propio directorio de base de datos de TDLib y su propio esquema MySQL.

Esto mantiene simples las consultas y las claves de Redis (chat:<id>:<key>) a costa de un proceso por tienda. Las claves de Redis no llevan prefijo de bot, así que dos instancias no deben compartir una misma base de datos Redis.

Ejecutar el bot sobre TDLib en lugar de la Bot API HTTPCliente TDLib en modo bot: una conexión persistente, un flujo de actualizaciones para ambas direcciones, cachés locales

El bot inicia sesión mediante TDLib (tdl con tdl-tdlib-addon) con un token de bot y mantiene una única conexión duradera con Telegram. Las actualizaciones se empujan a client.on('update'), de modo que el motor no necesita una URL de webhook accesible desde Telegram ni un bucle de sondeo getUpdates.

La razón principal es el modelo de pantallas. TDLib informa de los mensajes salientes del propio bot como updateNewMessage y updateMessageSendSucceeded en el mismo flujo que los mensajes del cliente. Un manejador registra cada ID de mensaje en un chat privado, enviado o recibido, y cuando una pantalla cambia el motor los elimina con deleteMessages (revoke: true). El chat conserva solo la pantalla actual, y las entradas escritas, como una consulta de búsqueda o una dirección, desaparecen con ella. El mismo flujo permite al motor esperar hasta que un álbum de fotos haya llegado realmente antes de enviar el panel del producto.

TDLib también cubre llamadas que la Bot API HTTP no tiene: getMessage vuelve a leer el texto de un mensaje que el bot envió antes, lo que usa el panel de productos agrupados para redibujar sus botones. getChat y getUser los responde el cliente TDLib con su base de datos de información de chat activada, y los medios salen desde rutas locales o por ID de archivo remoto mediante la base de datos de archivos. La base de datos de mensajes está desactivada porque el bot no guarda historial, el optimizador de almacenamiento poda los archivos de TDLib y TDLib ejecuta un hilo de trabajo.

El coste es que la semántica de TDLib se filtra a la aplicación: los ID de mensaje son temporales hasta que el servidor los confirma (véase la tarea de asignación de ID), y el directorio de sesión ata un bot a un proceso.

Posibles pasos siguientes que TDLib ya ofrece: activar su base de datos de mensajes para tener historial y búsqueda por chat, y enviar archivos mayores que el límite de subida de 50 MB de la Bot API HTTP. El motor no hace ninguna de las dos cosas hoy; limita los documentos a 50 MB.

Muchos módulos reaccionan a las mismas actualizaciones y pantallasBus de eventos para las actualizaciones más registros de hooks por chat y globales ordenados por prioridad

Answer convierte las actualizaciones de TDLib en eventos del bus: do_callback_query para pulsaciones de botones, do_command para texto, pre_checkout_query y successful_payment para pagos. El carrito, los favoritos, las reservas, el soporte y el resto se suscriben y manejan solo sus propias acciones, de modo que no importan los manejadores de los demás.

Para componer la interfaz hay dos registros de hooks. Hooks guarda callbacks por chat_id (el menú contextual actual, botones de producto adicionales) ordenados por prioridad, donde añadir un hook con una prioridad existente lo reemplaza. GlobalHooks hace lo mismo para todos los chats; la extensión de soporte lo usa para añadir un botón a paneles de producto que no le pertenecen. Los hooks por chat se descartan cuando se elimina el chat.

Activar y desactivar funciones del plan por tiendaLista de extensiones sincronizada desde la tienda; clase construida a partir del slug; reinicio con PM2 para descargar

La lista de extensiones activas (cupones, envío, soporte, reservas, marketing, b2b y otras) se sincroniza en la tabla de opciones. Extensions.init() convierte cada slug en un nombre de clase y la instancia antes de que se construyan los módulos principales, para que sus hooks existan primero. Los módulos principales comprueban una extensión mediante getters opcionales y cambian su comportamiento; por ejemplo, el pago omite el paso del código promocional cuando los cupones están desactivados.

Desactivar una extensión no puede descargar código que ya registró listeners, así que restablecer la caché de extensiones reinicia el proceso mediante PM2. Los comentarios del código señalan que esto interrumpe a los usuarios que están en plena sesión.

Permitir que la tienda empuje cambios y consultas a un bot en ejecuciónEndpoint HTTP /command; los comandos conocidos se manejan directamente, el resto se encamina a los módulos por el bus

Commander ejecuta un servidor http simple con /health y /command. Los comandos conocidos (payment_success, order_is_done, reset_cache, update_product_cache) se manejan directamente. Todo lo demás se emite como exec_command; el módulo que lo posee escribe is_executed y answer en el objeto body compartido, que el endpoint devuelve tras una breve espera fija.

reset_cache refresca una caché de ajustes o todas, con las extensiones siempre al final porque esa reinicia el proceso. El reset_db_cache() de cada módulo vuelve a extraer su blob JSON a la tabla de opciones y limpia su copia en memoria.

Mantener sano un proceso de larga duración en un servidor pequeñoPM2 en modo fork, heap de 450 MB, reinicio a 1 GB, de 2 a 5 conexiones a la base de datos, restablecimientos de caché temporizados

ecosystem.config.cjs ejecuta una única instancia en modo fork (el modo cluster está comentado; un token de bot implica una sesión TDLib), limita el old space de V8, reinicia ante el crecimiento de memoria y exige 30 s de actividad para que un arranque cuente como estable. Los manejadores de SIGTERM, SIGINT y SIGQUIT destruyen el pool de knex antes de salir, y un temporizador registra el uso del pool cuando hay actividad.

Las cachés en memoria, como los carritos y las divisas seleccionadas, se limpian con temporizadores, y el mapa de ID de mensajes se vacía en cada cambio de pantalla.

El chat como interfaz de aplicación

en el producto

Qué aportaLos clientes pasaban por el catálogo, el carrito y el pago como por pantallas de una aplicación, y el chat se mantenía ordenado en lugar de llenarse de mensajes antiguos.

Problema
Un chat de Telegram es un flujo de mensajes, pero el producto necesita pantallas: una página de catálogo, un carrito, un paso de pago, cada una reemplazando a la anterior, con botones que se actualizan en el sitio.
Solución

Seguir cada mensaje por chat, cambiar de pantalla eliminando el conjunto anterior después de dibujar el nuevo, asignar los ID temporales de TDLib a ID del servidor para ediciones posteriores, y mantener el estado de navegación en un contexto por chat en Redis.

Stack
  • TDLib
  • Redis
  • Teclados de respuesta e inline de Telegram
Tareas · 6
Editar un mensaje cuyo ID cambia tras el envíoAsignar ID temporales a ID del servidor a partir de updateMessageSendSucceeded, por chat en RAM

sendMessage en TDLib devuelve un ID local temporal; el ID real llega después en updateMessageSendSucceeded junto con old_message_id. El motor guarda el ID devuelto por send() en el contexto del chat (top_message_id, taxonomies_btn_mgs_id y similares) y lo resuelve mediante MessageMapping cuando necesita editar o eliminar ese mensaje.

La asignación vivió primero en Redis y se pasó a un mapa por chat en memoria, que se limpia en cada cambio de pantalla para que siga siendo pequeño. Sin ella, editar un teclado inline tras un clic apuntaría a un ID que ya no existe.

Hacer que un grupo de mensajes se comporte como una sola pantallaRegistrar en Redis los ID enviados; en una pantalla nueva apartarlos y eliminarlos tras dibujar

Cada nuevo ID de mensaje en un chat privado se añade a chat:<id>:messages. make_messages_castling() mueve esa lista a chat:<id>:old_messages y restablece la asignación de ID, el módulo dibuja la pantalla nueva y luego clear_display() elimina los mensajes antiguos. El contenido nuevo aparece antes de que desaparezca el antiguo, así que el chat nunca queda en blanco entre pantallas.

Se probó un borrado completo del chat y se dejó en el código solo como advertencia: en algunos teléfonos sacaba al usuario del bot.

Los mensajes con teclado de respuesta no se pueden editarEnviar un nuevo mensaje superior con el teclado actualizado y luego eliminar el antiguo

El menú inferior persistente muestra un botón de carrito con un contador de artículos en vivo. Telegram no permite editar el texto de un mensaje que lleva un teclado de respuesta, así que Cart.redraw_menu() envía un mensaje superior nuevo y elimina el anterior por su ID asignado, mientras que el resumen inferior, un mensaje inline, se edita en el sitio tras una breve espera.

El menú actual se guarda como un hook context_menu por chat. change_menu_buttons_text() reescribe solo el botón que empieza por el emoji del carrito en el menú serializado, de modo que cualquier pantalla puede actualizar el contador del carrito sin conocer su propia disposición.

Los textos de la tienda son HTML; TDLib quiere texto plano con desplazamientos de entidadesConversor propio: decodificar entidades, quitar un subconjunto de etiquetas, emitir desplazamientos textEntity

format_string_before_send() maneja strong, em, u, s, code, pre y a href. Encuentra la etiqueta de apertura más cercana, la quita junto con su etiqueta de cierre, y registra las posiciones de inicio y fin en la cadena limpia; estas se convierten en desplazamientos textEntity en formattedText. Las entidades HTML con nombre y numéricas de WooCommerce se decodifican primero. Una etiqueta mal formada o sin cerrar hace que la función devuelva el texto original sin entidades en lugar de enviar un formato roto.

Junto a ella hay dos trucos de maquetación: una serie de caracteres en blanco Braille rellena los mensajes para que las tarjetas de producto se muestren con el mismo ancho, y un carácter combinante superpuesto dibuja un precio antiguo tachado dentro de las etiquetas de los botones, donde no se permiten entidades.

El texto escrito puede ser una entrada del usuario o un botón de menú localizadoIndicador por chat de entrada esperada; las etiquetas de botones conocidas siempre salen de ese estado

Los botones del teclado de respuesta llegan como texto plano en el idioma del usuario, así que el enrutamiento compara el texto recibido con las etiquetas construidas por get_menu_command_text() para ese chat. Cuando un paso espera texto libre (consulta de búsqueda, SKU, dirección, código promocional, mensaje de soporte), el contexto contiene is_waiting_user_input y el comando a reanudar. Answer guarda el texto como user_message_text y repite ese comando, salvo que el texto sea un comando con barra o una etiqueta de navegación.

Los manejadores de entrada del pago repiten la comprobación con las etiquetas de su propio paso, de modo que pulsar Atrás mientras se espera una dirección navega hacia atrás en lugar de guardar esa palabra como la dirección.

Las tarjetas de producto deben llegar en orden y soportar una paginación rápidaEsperar la propia actualización del álbum antes de su panel; permitir una sola acción de paginación a la vez

Una tarjeta de producto es un álbum multimedia seguido de un panel de texto con botones. print_products_blocks() envía el álbum, espera hasta 10 s el evento new_message_coming de ese chat y luego hace una pausa antes de enviar el panel, de modo que el panel nunca aterriza por encima de sus fotos. Breves pausas fijas antes de cada envío y eliminación reparten las peticiones.

Los toques rápidos en siguiente y anterior iniciarían, de otro modo, un segundo cambio de pantalla antes de que el primero haya eliminado sus mensajes. Un indicador nowdo por chat ignora la navegación mientras se reemplaza una pantalla y se libera un segundo después de eliminar los mensajes antiguos.

Sincronización del catálogo de WooCommerce

en el producto

Qué aportaLos clientes paginaban el catálogo con rapidez y veían precios y stock actuales. Las ediciones en la tienda llegaban solas al bot, y los suscriptores se enteraban cuando los artículos volvían a estar disponibles.

Problema
La tienda funciona en un sitio WordPress en otro servidor. El bot debe paginar el catálogo con rapidez, mostrar precios y stock actuales y recoger las ediciones sin consultar la tienda en cada toque.
Solución

Mantener una copia MySQL de los productos y de los vínculos con categorías, rellenada por una importación completa paginada y actualizada por producto cuando la tienda envía un cambio. Los ajustes de la tienda llegan como blobs JSON en una tabla de opciones con cachés en memoria.

Stack
  • MySQL (MyISAM)
  • knex
  • WooCommerce REST via proxy
  • Telegram Web Apps
Tareas · 6
Acceder a los datos de cada tienda sin guardar sus credenciales RESTTodas las llamadas a la tienda van a un único proxy; dominio, endpoint, clave de API y HMAC viajan en cabeceras

get_rest_data() y send_remote_data() hacen POST a una única URL de proxy de Botoscope. La tienda se nombra en una cabecera client-domain, el endpoint en otra, y la petición se autentica con la clave de API del cliente y un pase derivado con HMAC-SHA256. Las descargas de archivos usan el mismo proxy mediante su endpoint fetch-file.

El motor nunca necesita las propias claves REST de la tienda, y el lado de WordPress solo tiene que responder al proxy. Los fallos devuelven null y quien llama trata null como ausencia de datos, de modo que una caída de la tienda se degrada a listas vacías en lugar de romper una pantalla.

Importar un catálogo completo bajo demanda, no en cada reinicioTruncar, desactivar claves, extraer páginas de 100, insertar por lotes, reactivar claves en finally

Products.reset_db_cache() lee el número de productos, trunca products y products_vs_terms, ejecuta ALTER TABLE ... DISABLE KEYS en ambas tablas MyISAM y extrae products/{offset}/{limit} en páginas de 100. Cada página se inserta en lotes de 100 filas; un error en una página se registra y la importación pasa a la siguiente. MyISAM no tiene transacciones, así que las filas ya escritas de una página fallida permanecen. Las claves se reactivan en un bloque finally, de modo que una importación fallida deja igualmente tablas indexadas. Mientras la importación se ejecuta, el bot muestra la parte del catálogo cargada hasta el momento.

La importación no se ejecuta al arrancar. Un comentario explica que con un catálogo grande podría bloquear el servidor tras cada reinicio, por lo que solo se ejecuta ante un comando reset_cache explícito de la tienda.

Los datos de los productos varían según el tipo y no dejan de crecerColumnas para lo que se filtra u ordena, todo lo demás en una columna JSON data

insert_product_batch() desestructura los campos que consulta el motor (price, sale_price, indicadores de stock, type, parent_id, publish_date, menu_order, mínimo, máximo y paso del carrito) en columnas y serializa el resto de la carga en data. Las listas de medios, descargas, días de acceso o la URL de un producto externo pueden añadirse desde la tienda sin cambiar el esquema. Los ID de categoría van tanto a una lista separada por comas en la fila como a products_vs_terms para búsquedas de categoría indexadas.

install.sql se ejecuta en cada arranque con CREATE TABLE IF NOT EXISTS, de modo que una instancia nueva aprovisiona su propio esquema.

Aplicar ediciones de un solo producto sin una reimportación completaLa tienda envía update_product_cache; producto, variaciones y términos se reescriben a partir de una única respuesta de la tienda

Cuando un producto cambia en WooCommerce, la tienda envía update_product_cache a /command. update_product_cache() obtiene products_filter/{id}, que devuelve el producto con sus variaciones, luego elimina la fila del producto, las filas con ese parent_id y sus vínculos de categoría, e inserta las filas nuevas. Una petición lleva el producto y sus variaciones, así que las filas se reescriben una vez por cambio.

El límite del plan cuenta solo los productos padre. La importación completa recorta los productos principales por página y conserva siempre sus variaciones; la vía incremental omite un producto nuevo una vez que el recuento de productos distintos en products_vs_terms alcanza el límite.

Los indicadores de stock se desajustan y los clientes quieren avisos de reposiciónConciliar los indicadores de en stock a partir de la cantidad cada 3 min; avisar a los suscriptores cada 2 h

Para los productos con stock gestionado, un temporizador cada tres minutos fija is_in_stock a partir de stock_quantity en ambos sentidos, de modo que el botón de compra sigue la cantidad real aunque un indicador enviado estuviera obsoleto. Los clientes pueden suscribirse a un producto agotado o a una sola variación; stock_notify guarda product_id y chat_id. Cada dos horas el motor encuentra los productos suscritos que han vuelto a estar disponibles, envía la tarjeta de nuevo a cada suscriptor y elimina la suscripción.

Cada consulta del catálogo compara además publish_date con la hora actual, de modo que los productos programados permanecen ocultos hasta su fecha.

El filtrado por facetas no cabe en botones de chatAbrir la página de filtros de la tienda como Telegram Web App; esta devuelve los ID coincidentes

El comando /filter y los botones Filter abren filter_page_url como Web App con el chat_id añadido. El filtrado se ejecuta en el lado de WordPress. El resultado vuelve al motor como un comando filtered_products_ids en /command, y Products imprime esos ID como un catálogo paginado normal en ese chat.

Esto mantiene el recuento de facetas y la lógica de atributos en un solo lugar, la tienda, mientras el chat sigue mostrando los resultados con su propio formato de tarjeta.

Carrito, variaciones y pago

en el producto

Qué aportaEl bot siguió las reglas de la propia tienda para cantidades, variaciones, reservas y descuentos, y el pedido en la tienda coincidió con lo que el cliente vio en el chat.

Problema
El carrito y el pago deben hacer cumplir las reglas de la tienda (pasos de cantidad, mínimos, stock, variaciones, franjas de reserva, descuentos) solo con botones y respuestas de texto breves, y el pedido de WooCommerce tiene que coincidir con lo que el cliente vio.
Solución

Los carritos viven en Redis tras una caché en RAM, las variaciones se eligen con un selector inline en cascada, y el pago es una máquina de pasos depurada según el contenido del carrito y las extensiones activas. El pedido se crea una vez y se modifica a medida que el cliente edita.

Stack
  • Redis
  • Node.js
  • moment
  • WooCommerce orders via proxy
Tareas · 6
El carrito se lee en casi cada renderizadoRedis por chat tras una caché RAM write-through; artículos caducados y eliminados depurados

Cart.get() sirve desde un mapa estático por chat y recurre a Redis (chat:<id>:cart); update() escribe en ambos. La etiqueta del botón del carrito, los totales y cada panel de producto leen el carrito, así que la caché ahorra una ida y vuelta a Redis cada vez. Los artículos pueden llevar una caducidad, usada por las reservas retenidas, y se descartan al leer una vez caducados. Al dibujar el carrito se eliminan primero los ID que ya no existen en el catálogo local, de modo que un producto eliminado en WooCommerce no pueda llegar al pago.

Los botones de cantidad respetan quantity_step, min_cart_count y el stock gestionado, y una variación sin valores propios los hereda del producto padre.

Elegir una variación con botones cuando no existen todas las combinacionesFilas de atributos en cascada; cada fila ofrece solo los términos que aún forman una variación real

get_buy_buttons_panel() lee la combinación de atributos de cada variación y ordena los atributos según el orden de atributos de la tienda. La primera fila lista todos los términos en uso; cada fila siguiente lista solo los términos que, junto con las elecciones anteriores, coinciden con al menos una variación existente. Un atributo con tres o más opciones puede mostrarse como una fila compacta anterior / actual / siguiente en lugar de una cuadrícula.

Cambiar un atributo superior borra los inferiores y salta a la primera combinación coincidente. Un producto se abre en su variación más barata. La elección se guarda por chat y producto en el contexto, y el panel se edita en el sitio en cada toque.

Los pasos del pago dependen de lo que hay en el carritoLista de pasos depurada por chat: sin envío para carritos digitales, sin paso de pago cuando es gratis

Los pasos base son código promocional, dirección de envío, método de envío, método de pago, comentario y confirmación. Los pasos de las extensiones desactivadas se eliminan al arrancar. Después get_checkout_steps() inspecciona el carrito, recorriendo los productos agrupados y las variaciones de sus hijos variables para encontrar algún artículo físico; un carrito sin ninguno omite los dos pasos de envío. Un total cero cambia a la pasarela de regalo y omite la elección de pago, una tienda con una sola pasarela la preselecciona, y pagar en Stars fuerza la pasarela de Stars.

Los pasos obligatorios bloquean next() hasta que se guarda un valor, y cualquier paso puede reabrirse en modo de edición desde la pantalla de confirmación y volver directamente a ella.

El pedido de la tienda debe seguir las ediciones hechas después de su creaciónCrear el pedido una vez en el paso de pago, guardar su ID en el contexto, enviar cada edición posterior

generate_payment_button() llama a create_order solo cuando el contexto aún no tiene order_id, y luego guarda el ID. Si el cliente retrocede y cambia el método de pago, la dirección, el código promocional o el comentario, cada manejador llama a orders.edit(order_id, ...) para que el pedido de WooCommerce se modifique en lugar de duplicarse.

El pedido lleva el chat_id de Telegram y el nombre del bot, de modo que el historial de pedidos se obtiene por chat y un enlace de pay-for-me puede localizar el pedido más tarde.

Dos clientes del bot no deben reservar la misma franja horariaRetenciones en memoria por franja durante 600 s, contadas frente a la capacidad restante de la franja

Las franjas llegan de la tienda por producto y día con capacity y capacity_used, y se filtran las franjas que la tienda marca como reservadas. Cuando un cliente elige una franja, el motor registra una retención con caducidad y añade la reserva al carrito con el mismo tiempo de retención. is_freezed() cuenta las retenciones activas de esa franja, fecha y clave, y bloquea la franja cuando las retenciones alcanzan la capacidad menos las plazas usadas. Quitar el artículo del carrito libera su retención mediante el evento done_remove_from_cart.

Las fechas del calendario y el filtrado de franjas pasadas usan la zona horaria de la tienda de los ajustes, no el reloj del servidor. Las retenciones viven en la memoria del proceso del bot, por lo que solo se aplican a los clientes de este bot y se restablecen al reiniciar; las reservas hechas en el sitio web en el mismo momento no se retienen.

Las promociones como 'cada 3.er artículo con un 25 % de descuento' necesitan un formato de reglasPequeño lenguaje de fórmulas: condiciones de número de artículos; descuento; n-ésimo, el más barato o el más caro, tope

Las campañas de marketing usan fórmulas como 'c >= 6 & c < 12; 25%; 3n, me, cap=3'. parse_formula() las divide en condiciones de número de artículos, un descuento porcentual o fijo y modificadores (cada n-ésima unidad, primero la más barata o la más cara, un tope). get_applied_amount() expande el carrito en una entrada por cada unidad de los productos de la campaña, aplica las fórmulas en orden y marca las unidades usadas para que una fórmula posterior no pueda volver a descontarlas.

Las ventanas de las campañas se comprueban en la zona horaria de la tienda, las campañas en modo de prueba se aplican solo al chat del administrador, y el descuento de campaña se omite cuando ya se aplica un cupón. Los cupones limitados a productos se extienden a los hijos de los productos agrupados y variables.

Pagos y divisas

en el producto

Qué aportaLos clientes veían los precios en su propia divisa y pagaban con tarjeta, criptomonedas o Telegram Stars, o pedían a un amigo que pagara. La tienda recibía un pedido pagado.

Problema
El pago empieza en el chat, termina en la página de una pasarela o dentro de Telegram, y debe acabar como un pedido de WooCommerce pagado, a través de varias pasarelas y divisas de visualización.
Solución

Los importes permanecen en la divisa base de la tienda y se convierten solo para mostrarlos y cobrarlos. El motor crea por sí mismo las sesiones de las pasarelas y sondea las pasarelas que no tienen webhook para el bot.

Stack
  • PayPal (sandbox)
  • Stripe Checkout
  • LiqPay
  • CoinGate
  • CryptoBot
  • Telegram Stars (TDLib invoices)
Tareas · 5
Precios mostrados en una divisa y cobrados en otraImportes guardados en divisa base, convertidos al mostrar y al cobrar, pedidos normalizados de vuelta

Los totales del carrito, los descuentos y el envío se calculan en la divisa base. format_price() convierte a la divisa seleccionada del chat con la tasa sincronizada y aplica la posición del símbolo y los decimales, redondeando hacia arriba cuando los céntimos están ocultos. generate_payment_button() recalcula el total a la divisa seleccionada antes de llamar a una pasarela. Los pedidos obtenidos de WooCommerce en otra divisa se dividen por su tasa de pedido almacenada, de modo que el historial y los totales coinciden con la aritmética del propio bot.

Cuando la tienda no permite pagar en otras divisas, la selección se restablece a la base en el paso de pago y se informa al cliente del motivo.

Aceptar Telegram Stars como método de pagoStars como divisa XTR de la tienda con una tasa; factura TDLib cuyo payload nombra el pedido

Stars se configura como una divisa llamada XTR en la lista de divisas de la tienda, así que su tasa llega por la misma sincronización. El importe es el total base multiplicado por la tasa XTR, redondeado hacia arriba. La factura se envía como inputMessageInvoice con un token de proveedor vacío y un payload JSON en base64 que contiene order_id, chat_id, amount y gateway. Se responde a la consulta de pre-checkout y, en messagePaymentSuccessfulBot, el payload se decodifica y se notifica a la tienda que el pedido está pagado.

Sin una divisa XTR, la opción de Stars informa de un error en lugar de emitir una factura.

Pasarela de criptomonedas sin webhook registrado para el botSondeo por chat cada 8 s hasta 15 min, cancelado cuando se reabre el carrito

Para CryptoBot el motor crea una factura denominada en moneda fiat con una caducidad de 900 s y sondea getInvoices cada 8 s. Un Map de temporizadores indexado por chat_id mantiene un sondeo activo por cliente, y abrir el carrito lo cancela. Al pagarse, el pedido pasa a processing y se vacía el carrito; si caduca o se cancela, el cliente recibe un mensaje.

CoinGate va en sentido contrario: su URL de callback apunta al proxy de Botoscope, que reenvía el webhook a /command como payment_success, y el pedido se finaliza cuando el webhook informa de que está pagado.

Cada pasarela de tarjetas necesita su propio formato de sesiónSesiones de Stripe y LiqPay construidas en el motor, abiertas como Web App o URL; PayPal en sandbox

Cada pasarela tiene su propio constructor: Stripe mediante una Checkout Session, LiqPay mediante un blob de datos en base64 firmado con SHA-1 sobre la clave privada, los datos y la clave privada. El enlace se envía como botón Web App o URL, opcionalmente precedido por botones de política de reembolso, privacidad y envío. El constructor de PayPal (credenciales de cliente OAuth y luego un pago con URL de retorno y cancelación) envía el pago al endpoint sandbox de PayPal en este código, así que solo sirve para pruebas.

Los métodos offline (datos de tarjeta bancaria, SWIFT, regalo, pay-for-me) muestran instrucciones traducidas y un botón de confirmación en lugar de un enlace.

Permitir que otro usuario de Telegram pague un pedidoUn deep link pay-for-me abre el pedido en el chat del pagador y avisa al comprador

Elegir pay-for-me termina el pago con un enlace t.me que inicia el bot con payforme_<order_id>. Cuando otro usuario lo abre, el motor carga el pedido desde la tienda, aplica la divisa del pedido, elimina los métodos offline y pay-for-me y muestra las pasarelas restantes para el total del pedido. Tras el pago, el chat del comprador recibe un mensaje con el nombre del pagador.

Medios, archivos e idiomas

en el producto

Qué aportaLas fotos y los vídeos de los productos se abrían con rapidez, los archivos comprados llegaban directamente al chat y los clientes leían el menú y el catálogo en su propio idioma.

Problema
Las imágenes y los archivos de los productos residen en la tienda, y enviarlos por Telegram en cada visualización es lento. El bot también descarga URL que proceden de los datos de la tienda. La interfaz y el catálogo tienen que leerse en el idioma del cliente.
Solución

Una caché local de medios con hash, descargas protegidas a través del proxy, reutilización de archivos ya alojados por Telegram y traducciones por capas: cadenas de interfaz incluidas, ajustes del propietario y traducciones por campo en los datos del catálogo.

Stack
  • TDLib file API
  • Node streams
  • crypto (MD5, HMAC)
  • JSON language packs
Tareas · 6
Volver a descargar las fotos de producto en cada visualización de una tarjetaCaché local con clave MD5 de la URL, dividida en carpetas de dos caracteres hex, enviada como archivos locales

send_media_gallery() calcula el hash de cada URL de medio, guarda el archivo en media_files_cache/<primeros dos caracteres hex>/<hash> en el primer uso y después entrega a TDLib la ruta local. Los archivos de cero bytes se eliminan y se omiten, un álbum está limitado a 8 elementos, y una variación sin imágenes propias recurre a los medios del producto padre.

Los enlaces a publicaciones de t.me omiten la caché y van a TDLib como archivos remotos.

El bot descarga URL tomadas de los datos de la tiendaLista de dominios permitidos, extensiones bloqueadas, límites de tamaño por tipo, archivos parciales eliminados

downloadFileToLocal() acepta solo URL http(s) del propio dominio del cliente o de hosts de Telegram, rechaza extensiones ejecutables y de script en enlaces directos, envía una petición HEAD para rechazar archivos por encima del límite (10 MB para imágenes y audio, 50 MB para vídeo y documentos) y luego transmite el archivo mediante el endpoint fetch-file del proxy con el mismo límite. Las descargas vacías, demasiado grandes o fallidas se eliminan del disco.

Los vídeos grandes de productos son lentos de enviar desde la tiendaPublicar el vídeo en un canal; el bot responde con su ID de archivo de Telegram para reutilizarlo

Answer vigila las publicaciones del canal. Cuando aparece un vídeo en un canal al que pertenece el bot, este responde con el ID de archivo remoto del vídeo. Un enlace de publicación t.me o ese ID puede usarse entonces como medio de un producto, y el motor lo envía como archivo remoto, de modo que Telegram lo sirve desde su propio almacenamiento sin una subida por cliente.

Entregar archivos digitales de pago dentro del chatTras el pago, enviar cada descarga mediante la obtención protegida, en caché bajo un nombre enmascarado

Cuando el carrito contenía productos descargables y el pedido está pagado en línea o es gratuito, do_after_pay() envía cada descarga de esos productos como documento. Los archivos se guardan en caché con un nombre derivado del ID de descarga por shiftUUID(), de modo que el nombre almacenado no es el ID de la tienda, y los documentos de más de 50 MB se rechazan. Los productos con acceso limitado muestran los días restantes, calculados a partir de la fecha de pago del pedido.

Las facturas PDF del endpoint de facturas de la tienda siguen la misma vía.

Texto de interfaz en 7 idiomas, editable por el propietario de la tiendaJSON incluido por idioma, superpuesto con los ajustes del propietario sincronizados, con inglés como respaldo

Siete archivos de idioma (en, es, pt, ru, pl, kz, ua) con las mismas 215 claves se cargan al arrancar. __() combina el archivo de idioma del chat con los ajustes que el propietario introdujo en la administración (sincronizados como interface_translations e indexados como s<number>); si aún falta una clave, reintenta con inglés más los ajustes. El idioma elegido se guarda por chat; en el primer contacto procede del language_code del cliente de Telegram cuando ese idioma está activado, y en caso contrario del valor por defecto de la tienda.

Como los botones del menú se identifican por su texto localizado, las etiquetas de un chat se resuelven en paralelo con Promise.all en cada comprobación de enrutamiento.

El contenido del catálogo y la búsqueda deben funcionar en cada idiomaTraducciones por campo en productos y términos; la búsqueda coincide con el título o con las traducciones

Los productos, categorías, marcas, campos meta, métodos de envío y de pago llevan un objeto translations. translate_to() devuelve 0 cuando el chat usa el idioma por defecto, de modo que el caso habitual omite cualquier búsqueda; en otro caso, get_field_translation() elige el campo traducido y recurre al original. La búsqueda por título ejecuta LIKE sobre la columna del título y sobre el JSON de traducciones almacenado, con un mínimo de 3 caracteres, de modo que una consulta en cualquier idioma activo encuentra el producto.

Las fechas en el meta de productos y los periodos de acceso se formatean con el idioma del chat como configuración regional.

Cifras

del producto
  • Idiomas del bot 7 Inglés, español, portugués, ruso, polaco, kazajo, ucraniano.
  • Métodos de pago en línea 5 Stripe, LiqPay, CoinGate, CryptoBot y Telegram Stars.
  • Ruta de respuesta push, sin sondeo TDLib mantiene una conexión persistente con Telegram: cada toque llega al bot al instante, sin salto por webhook ni retraso de sondeo.