Idioma

Entrar em contacto

SaaS · desde 2025

Botoscope®

SaaS que transforma uma loja WooCommerce num bot do Telegram com catálogo, carrinho, checkout e encomendas sincronizados com a loja.

Lançado
2025
Estado
Encerrado em 2026
Plataforma
Telegram
Loja
WooCommerce
  • Node.js (módulos ES)
  • TDLib via tdl / tdl-tdlib-addon
  • MySQL / MariaDB (knex, mysql2)
  • Redis (ioredis)
  • PM2
  • axios
  • moment
  • Telegram Web Apps
  • WooCommerce via proxy do Botoscope
Abrir botoscope.com botoscope.com

Sobre

O Botoscope gere uma loja WooCommerce como um bot do Telegram. O desenvolvimento começou em 2024; o serviço foi lançado publicamente em 2025. Os clientes percorrem categorias, escolhem variações, enchem o carrinho, finalizam a compra e pagam dentro da conversa, e a encomenda chega ao WooCommerce. O motor aqui descrito é o processo Node.js que serve um bot. Comunica com o Telegram através do TDLib em modo bot, com a loja através de um proxy do Botoscope, mantém uma cópia MySQL do catálogo e guarda o estado por conversa no Redis.

Grande parte da dificuldade vem da própria conversa. Os IDs das mensagens mudam após o envio, os teclados de resposta não podem ser editados, cada toque num botão é um payload de callback curto, o texto escrito tanto pode ser uma entrada como o rótulo de um menu, e as mensagens antigas acumulam-se. Uma boa parte do motor existe para fazer um fluxo de mensagens comportar-se como os ecrãs de uma aplicação.

O resto é a consistência com uma loja noutro servidor: uma importação do catálogo que não corre no arranque, atualizações por produto enviadas a partir do WordPress, stock e preços que coincidem com os da loja, uma encomenda WooCommerce por checkout que acompanha as edições do cliente, e pagamento através de processadores de cartões, gateways de criptomoedas e Telegram Stars registado nessa encomenda.

Botoscope · loja no Telegram

o que foi resolvido

ENCERRADOdesde 2025

Motor e modelo de processos

no produto

O que ofereceCada loja teve o seu próprio bot, separado das outras lojas, que correu durante semanas num servidor pequeno e acolheu as alterações feitas pelo proprietário na loja.

Problema
Cada loja precisa do seu próprio bot, com a sua sessão do Telegram, catálogo e definições, a partir de uma única base de código. O processo tem de correr durante semanas em servidores pequenos e aceitar comandos do lado da loja.
Solução

Uma instância do motor por bot: um cliente TDLib, uma cópia MySQL do catálogo e estado no Redis, configurados por ambiente e executados sob PM2 com limites de memória. Os módulos comunicam por um bus de eventos e registos de hooks; as funcionalidades pagas carregam-se como extensões; o lado da loja chega ao bot por um pequeno endpoint HTTP de comandos.

Stack
  • Node.js
  • TDLib (tdl)
  • EventEmitter
  • PM2
  • knex
  • ioredis
Tarefas · 6
Isolar as lojas sem um caminho de código multi-tenantUm processo por bot; token, domínio da loja, chaves e limites do plano vêm do ambiente

Não há um ID de tenant em lado nenhum do motor. init() inicia sessão num cliente bot TDLib, abre um pool knex e uma ligação Redis, e lê o domínio da loja, a chave de API do cliente, o limite de produtos e a flag do plano gratuito a partir de variáveis de ambiente. O isolamento entre lojas é uma questão de implementação: cada bot tem a sua própria instância, com o seu próprio diretório de base de dados TDLib e esquema MySQL.

Isto mantém simples as consultas e as chaves Redis (chat:<id>:<key>) ao custo de um processo por loja. As chaves Redis não levam prefixo do bot, pelo que duas instâncias não podem partilhar a mesma base de dados Redis.

Executar o bot sobre o TDLib em vez da Bot API por HTTPCliente TDLib em modo bot: uma ligação persistente, um fluxo de atualizações para os dois sentidos, caches locais

O bot inicia sessão através do TDLib (tdl com tdl-tdlib-addon) com um token de bot e mantém uma única ligação duradoura ao Telegram. As atualizações são empurradas para client.on('update'), pelo que o motor não precisa de um URL de webhook acessível a partir do Telegram nem de um ciclo de polling com getUpdates.

A razão principal é o modelo de ecrãs. O TDLib comunica as mensagens enviadas pelo próprio bot como updateNewMessage e updateMessageSendSucceeded no mesmo fluxo das mensagens do cliente. Um único handler regista cada ID de mensagem numa conversa privada, enviada ou recebida, e quando um ecrã muda o motor elimina-as com deleteMessages (revoke: true). A conversa guarda apenas o ecrã atual, e as entradas escritas, como uma pesquisa ou uma morada, desaparecem com ele. O mesmo fluxo permite ao motor esperar até um álbum de fotografias ter realmente chegado antes de enviar o painel do produto.

O TDLib cobre também chamadas que a Bot API por HTTP não tem: getMessage relê o texto de uma mensagem que o bot enviou antes, o que o painel de produtos agrupados usa para redesenhar os seus botões. getChat e getUser são respondidos pelo cliente TDLib com a base de dados de informação de conversas ligada, e os ficheiros multimédia saem de caminhos locais ou por ID de ficheiro remoto através da base de dados de ficheiros. A base de dados de mensagens está desligada porque o bot não guarda histórico, o otimizador de armazenamento poda os ficheiros do TDLib, e o TDLib corre com uma thread de trabalho.

O custo é que a semântica do TDLib se infiltra na aplicação: os IDs das mensagens são provisórios até o servidor os confirmar (ver a tarefa de mapeamento de IDs), e o diretório de sessão prende um bot a um processo.

Possíveis passos seguintes que o TDLib já oferece: ligar a sua base de dados de mensagens para histórico e pesquisa por conversa, e enviar ficheiros maiores do que o limite de 50 MB de envio da Bot API por HTTP. O motor não faz nenhuma das duas hoje; limita os documentos a 50 MB.

Muitos módulos reagem às mesmas atualizações e ecrãsBus de eventos para atualizações mais registos de hooks por conversa e globais, ordenados por prioridade

Answer transforma as atualizações do TDLib em eventos do bus: do_callback_query para toques em botões, do_command para texto, pre_checkout_query e successful_payment para pagamentos. Carrinho, favoritos, reservas, suporte e os restantes subscrevem e tratam apenas as suas próprias ações, pelo que não importam os handlers uns dos outros.

Para a composição da interface há dois registos de hooks. Hooks guarda callbacks por chat_id (o menu de contexto atual, botões extra de produto) ordenados por prioridade, onde acrescentar um hook com uma prioridade existente o substitui. GlobalHooks faz o mesmo para todas as conversas; a extensão de suporte usa-o para acrescentar um botão a painéis de produto que não são seus. Os hooks por conversa são largados quando a conversa é eliminada.

Ligar e desligar as funcionalidades do plano por lojaLista de extensões sincronizada a partir da loja; classe construída a partir do slug; reinício com PM2 para descarregar

A lista de extensões ativas (cupões, envio, suporte, reservas, marketing, b2b e outras) é sincronizada para a tabela de opções. Extensions.init() converte cada slug num nome de classe e instancia-a antes de os módulos principais serem construídos, para que os seus hooks existam primeiro. Os módulos principais verificam uma extensão através de getters opcionais e mudam de comportamento; por exemplo, o checkout salta o passo do código promocional quando os cupões estão desligados.

Desativar uma extensão não consegue descarregar código que já registou listeners, pelo que repor a cache das extensões reinicia o processo através do PM2. Os comentários do código notam que isto interrompe os utilizadores a meio de uma sessão.

Permitir que o lado da loja envie alterações e consultas a um bot em execuçãoEndpoint HTTP /command; comandos conhecidos tratados diretamente, os restantes encaminhados para os módulos pelo bus

Commander corre um servidor http simples com /health e /command. Os comandos conhecidos (payment_success, order_is_done, reset_cache, update_product_cache) são tratados diretamente. Tudo o resto é emitido como exec_command; o módulo que o possui escreve is_executed e answer no objeto body partilhado, que o endpoint devolve após uma curta espera fixa.

reset_cache atualiza uma cache de definições ou todas, com as extensões sempre em último porque essa reinicia o processo. O reset_db_cache() de cada módulo volta a puxar o seu blob JSON para a tabela de opções e limpa a sua cópia em memória.

Manter saudável um processo de longa duração num servidor pequenoPM2 em modo fork, heap de 450 MB, reinício a 1 GB, 2 a 5 ligações à BD, limpezas de cache temporizadas

ecosystem.config.cjs corre uma única instância em fork (o modo cluster está comentado; um token de bot significa uma sessão TDLib), limita o espaço antigo do V8, reinicia perante crescimento de memória e exige 30 s de funcionamento antes de um arranque contar como estável. Os handlers de SIGTERM, SIGINT e SIGQUIT destroem o pool knex antes de sair, e um temporizador regista o uso do pool quando há atividade.

As caches em memória, como carrinhos e moedas selecionadas, são limpas por temporizadores, e o mapa de IDs de mensagens é esvaziado a cada mudança de ecrã.

A conversa como interface de aplicação

no produto

O que ofereceOs clientes percorriam catálogo, carrinho e checkout como ecrãs de uma aplicação, e a conversa ficava arrumada em vez de se encher de mensagens antigas.

Problema
Uma conversa do Telegram é um fluxo de mensagens, mas o produto precisa de ecrãs: uma página de catálogo, um carrinho, um passo de checkout, cada um a substituir o anterior, com botões que se atualizam no próprio local.
Solução

Seguir cada mensagem por conversa, trocar de ecrã eliminando o conjunto anterior depois de o novo estar desenhado, mapear os IDs provisórios do TDLib para IDs do servidor para edições posteriores, e manter o estado de navegação num contexto por conversa no Redis.

Stack
  • TDLib
  • Redis
  • Teclados de resposta e inline do Telegram
Tarefas · 6
Editar uma mensagem cujo ID muda após o envioMapear IDs provisórios para IDs do servidor a partir de updateMessageSendSucceeded, por conversa em RAM

sendMessage no TDLib devolve um ID local provisório; o ID real chega depois em updateMessageSendSucceeded, juntamente com old_message_id. O motor guarda o ID devolvido por send() no contexto da conversa (top_message_id, taxonomies_btn_mgs_id e semelhantes) e resolve-o através de MessageMapping quando precisa de editar ou eliminar essa mensagem.

O mapeamento viveu primeiro no Redis e passou para um mapa por conversa em memória, limpo a cada mudança de ecrã para se manter pequeno. Sem ele, editar um teclado inline após um clique apontaria a um ID que já não existe.

Fazer um grupo de mensagens comportar-se como um só ecrãRegistar os IDs enviados no Redis; num novo ecrã, pô-los de parte e eliminá-los depois de desenhar

Cada novo ID de mensagem numa conversa privada é empurrado para chat:<id>:messages. make_messages_castling() move essa lista para chat:<id>:old_messages e repõe o mapeamento de IDs, o módulo desenha o novo ecrã, e clear_display() elimina depois as mensagens antigas. O novo conteúdo aparece antes de o antigo desaparecer, pelo que a conversa nunca fica em branco entre ecrãs.

Experimentou-se uma limpeza completa da conversa, mantida no código apenas como aviso: em alguns telemóveis expulsava o utilizador do bot.

As mensagens com teclado de resposta não podem ser editadasEnviar uma nova mensagem de topo com o teclado atualizado e depois eliminar a antiga

O menu persistente inferior mostra um botão de carrinho com a contagem de artigos em tempo real. O Telegram não permite editar o texto de uma mensagem que leva um teclado de resposta, pelo que Cart.redraw_menu() envia uma nova mensagem de topo e elimina a anterior pelo seu ID mapeado, enquanto o resumo inferior, uma mensagem inline, é editado no próprio local após um curto atraso.

O menu atual é guardado como um hook context_menu por conversa. change_menu_buttons_text() reescreve apenas o botão que começa pelo emoji do carrinho no menu serializado, de modo que qualquer ecrã possa atualizar a contagem do carrinho sem conhecer a sua própria disposição.

Os textos da loja são HTML; o TDLib quer texto simples com offsets de entidadesConversor próprio: descodificar entidades, retirar um subconjunto de tags, emitir offsets de textEntity

format_string_before_send() trata strong, em, u, s, code, pre e a href. Encontra a tag de abertura mais próxima, remove-a juntamente com a de fecho, e regista as posições de início e fim na string limpa; estas tornam-se offsets de textEntity em formattedText. As entidades HTML nomeadas e numéricas do WooCommerce são descodificadas primeiro. Uma tag malformada ou por fechar faz a função devolver o texto original sem entidades em vez de enviar formatação quebrada.

Dois truques de disposição ficam a seu lado: uma sequência de carateres em branco Braille preenche as mensagens para que os cartões de produto tenham a mesma largura, e um caráter de sobreposição combinante desenha um preço antigo riscado dentro dos rótulos dos botões, onde as entidades não são permitidas.

O texto escrito pode ser entrada do utilizador ou um botão de menu localizadoFlag por conversa para entrada esperada; os rótulos de botões conhecidos saem sempre dela

Os botões do teclado de resposta chegam como texto simples no idioma do utilizador, pelo que o encaminhamento compara o texto recebido com os rótulos construídos por get_menu_command_text() para essa conversa. Quando um passo espera texto livre (pesquisa, SKU, morada, código promocional, mensagem de suporte), o contexto guarda is_waiting_user_input e o comando a retomar. Answer guarda o texto como user_message_text e repete esse comando, a menos que o texto seja um comando com barra ou um rótulo de navegação.

Os handlers de entrada do checkout repetem a verificação contra os rótulos dos seus próprios passos, pelo que premir Voltar quando se espera uma morada navega para trás em vez de guardar a palavra como morada.

Os cartões de produto têm de chegar por ordem e sobreviver a uma paginação rápidaEsperar pela atualização do próprio álbum antes do seu painel; permitir uma ação de paginação de cada vez

Um cartão de produto é um álbum multimédia seguido de um painel de texto com botões. print_products_blocks() envia o álbum, espera até 10 s pelo evento new_message_coming dessa conversa, e faz depois uma pausa antes de enviar o painel, para que o painel nunca fique acima das suas fotografias. Curtas pausas fixas antes de cada envio e eliminação espaçam os pedidos.

Toques rápidos em seguinte e anterior iniciariam de outro modo uma segunda troca de ecrã antes de a primeira ter eliminado as suas mensagens. Uma flag nowdo por conversa ignora a navegação enquanto um ecrã está a ser substituído e é libertada um segundo depois de as mensagens antigas serem eliminadas.

Sincronização do catálogo WooCommerce

no produto

O que ofereceOs clientes paginavam o catálogo rapidamente e viam preços e stock atuais. As edições na loja chegavam ao bot por si, e os subscritores sabiam quando os artigos voltavam a ter stock.

Problema
A loja corre num site WordPress noutro servidor. O bot tem de paginar o catálogo rapidamente, mostrar preços e stock atuais e acolher edições sem consultar a loja a cada toque.
Solução

Manter uma cópia MySQL dos produtos e das ligações a categorias, preenchida por uma importação completa paginada e atualizada por produto quando a loja envia uma alteração. As definições da loja chegam como blobs JSON numa tabela de opções com caches em memória.

Stack
  • MySQL (MyISAM)
  • knex
  • WooCommerce REST via proxy
  • Telegram Web Apps
Tarefas · 6
Aceder aos dados de cada loja sem guardar as suas credenciais RESTTodas as chamadas à loja vão para um proxy; domínio, endpoint, chave de API e HMAC passam em cabeçalhos

get_rest_data() e send_remote_data() fazem post para um único URL de proxy do Botoscope. A loja é indicada num cabeçalho client-domain, o endpoint noutro, e o pedido é autenticado com a chave de API do cliente e um código derivado com HMAC-SHA256. Os descarregamentos de ficheiros usam o mesmo proxy através do seu endpoint fetch-file.

O motor nunca precisa das chaves REST da própria loja, e o lado WordPress só tem de responder ao proxy. As falhas devolvem null e quem chama trata null como ausência de dados, pelo que uma indisponibilidade da loja degrada-se para listas vazias em vez de fazer falhar um ecrã.

Importar um catálogo completo a pedido, não a cada reinícioTruncar, desativar chaves, puxar páginas de 100, inserir em lotes, reativar chaves em finally

Products.reset_db_cache() lê a contagem de produtos, trunca products e products_vs_terms, executa ALTER TABLE ... DISABLE KEYS nas duas tabelas MyISAM e puxa products/{offset}/{limit} em páginas de 100. Cada página é inserida em lotes de 100 linhas; um erro numa página é registado e a importação passa à página seguinte. O MyISAM não tem transações, pelo que as linhas já escritas de uma página falhada permanecem. As chaves são reativadas num bloco finally, de modo que uma importação falhada deixa na mesma tabelas indexadas. Enquanto a importação corre, o bot mostra a parte do catálogo carregada até ao momento.

A importação não corre no arranque. Um comentário explica que, com um catálogo grande, poderia bloquear o servidor após cada reinício, pelo que corre apenas num comando reset_cache explícito vindo da loja.

Os payloads de produtos diferem por tipo e continuam a crescerColunas para o que é filtrado ou ordenado, tudo o resto numa coluna JSON data

insert_product_batch() desestrutura os campos que o motor consulta (price, sale_price, flags de stock, type, parent_id, publish_date, menu_order, mínimo, máximo e passo do carrinho) em colunas e serializa o resto do payload em data. Listas de multimédia, downloads, dias de acesso ou o URL de um produto externo podem ser acrescentados do lado da loja sem alterar o esquema. Os IDs de categoria vão tanto para uma lista separada por vírgulas na linha como para products_vs_terms, para consultas indexadas de categorias.

install.sql corre em cada arranque com CREATE TABLE IF NOT EXISTS, pelo que uma nova instância cria o seu próprio esquema.

Aplicar edições de um só produto sem uma reimportação completaA loja envia update_product_cache; produto, variações e termos reescritos a partir de uma resposta da loja

Quando um produto muda no WooCommerce, o lado da loja envia update_product_cache para /command. update_product_cache() vai buscar products_filter/{id}, que devolve o produto com as suas variações, elimina depois a linha do produto, as linhas com esse parent_id e as suas ligações a categorias, e insere as novas linhas. Um único pedido traz o produto e as suas variações, pelo que as linhas são reescritas uma vez por alteração.

O limite do plano conta apenas os produtos-pai. A importação completa corta os produtos principais por página e mantém sempre as suas variações; o caminho incremental ignora um novo produto quando a contagem de produtos distintos em products_vs_terms atinge o limite.

As flags de stock divergem e os clientes querem alertas de regresso ao stockReconciliar as flags de em stock a partir da quantidade a cada 3 min; notificar subscritores a cada 2 h

Para produtos com stock gerido, um temporizador a cada três minutos define is_in_stock a partir de stock_quantity nos dois sentidos, de modo que o botão de compra segue a quantidade real mesmo quando uma flag enviada estava desatualizada. Os clientes podem subscrever um produto esgotado ou uma única variação; stock_notify guarda product_id e chat_id. De duas em duas horas o motor encontra os produtos subscritos que voltaram a ter stock, envia de novo o cartão a cada subscritor e remove a subscrição.

Cada consulta ao catálogo compara também publish_date com a hora atual, de modo que os produtos agendados ficam ocultos até à sua data.

A filtragem por facetas não cabe em botões de conversaAbrir a página de filtros da loja como Telegram Web App; esta devolve os IDs encontrados

O comando /filter e os botões Filtrar abrem filter_page_url como Web App com o chat_id anexado. A filtragem corre do lado do WordPress. O resultado regressa ao motor como um comando filtered_products_ids em /command, e Products imprime esses IDs como um catálogo paginado normal nessa conversa.

Isto mantém a contagem de facetas e a lógica de atributos num só sítio, na loja, enquanto a conversa continua a mostrar os resultados no seu próprio formato de cartão.

Carrinho, variações e checkout

no produto

O que ofereceO bot seguiu as regras da própria loja para quantidades, variações, reservas e descontos, e a encomenda na loja correspondeu ao que o cliente viu na conversa.

Problema
O carrinho e o checkout têm de fazer cumprir as regras da loja (passos de quantidade, mínimos, stock, variações, intervalos de reserva, descontos) apenas com botões e respostas curtas de texto, e a encomenda WooCommerce tem de corresponder ao que o cliente viu.
Solução

Os carrinhos vivem no Redis atrás de uma cache em RAM, as variações são escolhidas com um seletor inline em cascata, e o checkout é uma máquina de passos podada pelo conteúdo do carrinho e pelas extensões ativas. A encomenda é criada uma vez e corrigida à medida que o cliente edita.

Stack
  • Redis
  • Node.js
  • moment
  • Encomendas WooCommerce via proxy
Tarefas · 6
O carrinho é lido em quase todos os desenhos de ecrãRedis por conversa atrás de uma cache RAM write-through; artigos expirados e eliminados podados

Cart.get() serve a partir de um mapa estático por conversa e recorre ao Redis (chat:<id>:cart); update() escreve em ambos. O rótulo do botão do carrinho, os totais e cada painel de produto leem o carrinho, pelo que a cache poupa uma ida e volta ao Redis de cada vez. Os artigos podem ter uma validade, usada pelas retenções de reservas, e são largados na leitura quando expiram. Desenhar o carrinho remove primeiro os IDs que já não existem no catálogo local, de modo que um produto eliminado no WooCommerce não pode chegar ao checkout.

Os botões de quantidade respeitam quantity_step, min_cart_count e o stock gerido, e uma variação sem valores próprios herda-os do produto-pai.

Escolher uma variação por botões quando nem todas as combinações existemLinhas de atributos em cascata; cada linha oferece apenas os termos que ainda formam uma variação real

get_buy_buttons_panel() lê a combinação de atributos de cada variação e ordena os atributos pela ordem de atributos da loja. A primeira linha lista todos os termos em uso; cada linha seguinte lista apenas os termos que, juntamente com as escolhas acima, correspondem a pelo menos uma variação existente. Um atributo com três ou mais opções pode ser mostrado como uma linha compacta anterior / atual / seguinte em vez de uma grelha.

Mudar um atributo superior limpa os inferiores e ajusta-se à primeira combinação correspondente. Um produto abre na sua variação mais barata. A escolha é guardada por conversa e produto no contexto, e o painel é editado no próprio local a cada toque.

Os passos do checkout dependem do que está no carrinhoLista de passos podada por conversa: sem envio para carrinhos digitais, sem passo de pagamento quando é grátis

Os passos base são código promocional, morada de envio, método de envio, método de pagamento, comentário e confirmar. Os passos das extensões desativadas são removidos no arranque. get_checkout_steps() inspeciona então o carrinho, percorrendo os produtos agrupados e as variações dos seus filhos variáveis para encontrar qualquer artigo físico; um carrinho sem nenhum salta os dois passos de envio. Um total zero passa para a gate de oferta e salta a escolha de pagamento, uma loja com uma só gateway pré-seleciona-a, e pagar em Stars força a gate Stars.

Os passos obrigatórios bloqueiam next() até um valor ser guardado, e qualquer passo pode ser reaberto em modo de edição a partir do ecrã de confirmação e regressar diretamente a este.

A encomenda da loja tem de acompanhar as edições feitas depois de existirCriar a encomenda uma vez no passo de pagamento, guardar o seu ID no contexto, enviar cada edição posterior

generate_payment_button() chama create_order apenas quando o contexto ainda não tem order_id, e guarda depois o ID. Se o cliente voltar atrás e mudar o método de pagamento, a morada, o código promocional ou o comentário, cada handler chama orders.edit(order_id, ...) para que a encomenda WooCommerce seja corrigida em vez de duplicada.

A encomenda leva o chat_id do Telegram e o nome do bot, de modo que o histórico de encomendas é obtido por conversa e uma ligação pay-for-me pode localizar a encomenda mais tarde.

Dois clientes do bot não podem reservar o mesmo intervalo de tempoRetenções em memória por intervalo durante 600 s, contadas contra a capacidade restante do intervalo

Os intervalos vêm da loja por produto e dia com capacity e capacity_used, e os intervalos que a loja marca como reservados são filtrados. Quando um cliente escolhe um intervalo, o motor regista uma retenção com validade e adiciona a reserva ao carrinho com o mesmo tempo de retenção. is_freezed() conta as retenções ativas para esse intervalo, data e chave e bloqueia o intervalo quando as retenções atingem a capacidade menos os lugares usados. Remover o artigo do carrinho liberta a sua retenção através do evento done_remove_from_cart.

As datas do calendário e a filtragem de intervalos passados usam o fuso horário da loja nas definições, não o relógio do servidor. As retenções vivem na memória do processo do bot, pelo que se aplicam apenas aos clientes deste bot e são repostas num reinício; as reservas feitas no site ao mesmo tempo não são retidas.

Promoções como 'cada 3.º artigo com 25% de desconto' precisam de um formato de regraPequena linguagem de fórmulas: condições de contagem de artigos; desconto; n-ésimo, mais barato ou mais caro, limite

As campanhas de marketing usam fórmulas como 'c >= 6 & c < 12; 25%; 3n, me, cap=3'. parse_formula() divide-as em condições de contagem de artigos, um desconto percentual ou fixo, e modificadores (cada n-ésima unidade, mais barato ou mais caro primeiro, um limite). get_applied_amount() expande o carrinho numa entrada por unidade de produtos da campanha, aplica as fórmulas por ordem e marca as unidades usadas para que uma fórmula posterior não as volte a descontar.

As janelas das campanhas são verificadas no fuso horário da loja, as campanhas em modo de teste aplicam-se apenas à conversa do administrador, e o desconto da campanha é ignorado quando já se aplica um cupão. Os cupões com âmbito de produto estendem-se aos filhos dos produtos agrupados e variáveis.

Pagamentos e moedas

no produto

O que ofereceOs clientes viam os preços na sua própria moeda e pagavam com cartão, criptomoedas ou Telegram Stars, ou pediam a um amigo para pagar. A loja recebia uma encomenda paga.

Problema
O pagamento começa na conversa, termina numa página de gateway ou dentro do Telegram, e tem de acabar como uma encomenda WooCommerce paga, através de várias gateways e moedas de apresentação.
Solução

Os montantes permanecem na moeda base da loja e são convertidos apenas para apresentação e cobrança. O motor cria ele próprio as sessões das gateways e faz polling das gateways que não têm webhook para o bot.

Stack
  • PayPal (sandbox)
  • Stripe Checkout
  • LiqPay
  • CoinGate
  • CryptoBot
  • Telegram Stars (faturas TDLib)
Tarefas · 5
Preços mostrados numa moeda, cobrados noutraMontantes mantidos na moeda base, convertidos na apresentação e na cobrança, encomendas normalizadas de volta

Os totais do carrinho, os descontos e o envio são calculados na moeda base. format_price() converte para a moeda selecionada da conversa com a taxa sincronizada e aplica a posição do símbolo e as casas decimais, arredondando para cima quando os cêntimos estão ocultos. generate_payment_button() recalcula o total na moeda selecionada antes de chamar uma gateway. As encomendas obtidas do WooCommerce noutra moeda são divididas pela sua taxa de encomenda guardada, de modo que o histórico e os totais coincidem com a aritmética do próprio bot.

Quando a loja não permite pagar noutras moedas, a seleção é reposta na moeda base no passo de pagamento e o cliente é informado do motivo.

Aceitar Telegram Stars como método de pagamentoStars como moeda da loja XTR com taxa; fatura TDLib cujo payload nomeia a encomenda

As Stars são configuradas como uma moeda chamada XTR na lista de moedas da loja, pelo que a sua taxa chega pela mesma sincronização. O montante é o total base vezes a taxa XTR, arredondado para cima. A fatura é enviada como inputMessageInvoice com um token de fornecedor vazio e um payload JSON em base64 com order_id, chat_id, amount e gateway. A consulta de pré-checkout é respondida e, em messagePaymentSuccessfulBot, o payload é descodificado e a loja é notificada de que a encomenda está paga.

Sem uma moeda XTR, a opção Stars comunica um erro em vez de uma fatura.

Gateway de criptomoedas sem webhook registado para o botPolling por conversa a cada 8 s durante até 15 min, cancelado quando o carrinho é reaberto

Para o CryptoBot, o motor cria uma fatura denominada em moeda fiduciária com validade de 900 s e faz polling de getInvoices a cada 8 s. Um Map de temporizadores indexado por chat_id mantém um polling ativo por cliente, e abrir o carrinho cancela-o. Quando pago, a encomenda passa a processing e o carrinho é limpo; quando expirada ou cancelada, o cliente recebe uma mensagem.

O CoinGate segue o caminho inverso: o seu URL de callback aponta para o proxy do Botoscope, que reencaminha o webhook para /command como payment_success, e a encomenda é concluída quando o webhook comunica pago.

As gateways de cartão precisam cada uma do seu formato de sessãoSessões Stripe e LiqPay construídas no motor, abertas como Web App ou URL; PayPal em sandbox

Cada gateway tem o seu próprio construtor: Stripe através de uma Checkout Session, LiqPay através de um blob de dados em base64 assinado com SHA-1 sobre chave privada, dados e chave privada. A ligação é enviada como botão Web App ou URL, opcionalmente precedida por botões das políticas de reembolso, privacidade e envio. O construtor do PayPal (credenciais de cliente OAuth, depois um pagamento com URLs de retorno e cancelamento) envia o pagamento para o endpoint sandbox do PayPal neste código, pelo que serve apenas para testes.

Os métodos offline (dados de cartão bancário, SWIFT, oferta, pay-for-me) mostram instruções traduzidas e um botão de confirmação em vez de uma ligação.

Deixar outro utilizador do Telegram pagar uma encomendaDeep link pay-for-me abre a encomenda na conversa de quem paga e notifica o comprador

Escolher pay-for-me termina o checkout com uma ligação t.me que inicia o bot com payforme_<order_id>. Quando outro utilizador a abre, o motor carrega a encomenda da loja, aplica a moeda da encomenda, remove os métodos offline e pay-for-me e mostra as gateways restantes para o total da encomenda. Após o pagamento, a conversa do comprador recebe uma mensagem que nomeia quem pagou.

Multimédia, ficheiros e idiomas

no produto

O que ofereceAs fotografias e os vídeos dos produtos abriam depressa, os ficheiros comprados chegavam diretamente à conversa, e os clientes liam o menu e o catálogo no seu idioma.

Problema
As imagens e os ficheiros dos produtos vivem na loja, e enviá-los pelo Telegram a cada visualização é lento. O bot descarrega também URLs que vêm de dados da loja. A interface e o catálogo têm de ler-se no idioma do cliente.
Solução

Uma cache local de multimédia com hash, descarregamentos protegidos através do proxy, reutilização de ficheiros já alojados pelo Telegram, e traduções em camadas: textos de interface incluídos, substituições do proprietário e traduções por campo nos dados do catálogo.

Stack
  • API de ficheiros do TDLib
  • Node streams
  • crypto (MD5, HMAC)
  • Pacotes de idioma JSON
Tarefas · 6
Voltar a descarregar as fotografias do produto a cada visualização de cartãoCache local indexada pelo MD5 do URL, repartida em pastas de dois carateres hex, enviada como ficheiros locais

send_media_gallery() faz o hash de cada URL de multimédia, guarda o ficheiro em media_files_cache/<first two hex chars>/<hash> na primeira utilização e depois entrega ao TDLib o caminho local. Os ficheiros de zero bytes são eliminados e ignorados, um álbum é limitado a 8 itens, e uma variação sem imagens próprias recorre à multimédia do produto-pai.

As ligações a publicações t.me ignoram a cache e vão para o TDLib como ficheiros remotos.

O bot descarrega URLs retirados de dados da lojaLista de domínios permitidos, extensões bloqueadas, limites de tamanho por tipo, ficheiros parciais removidos

downloadFileToLocal() aceita apenas URLs http(s) no domínio do próprio cliente ou em hosts do Telegram, rejeita extensões de executáveis e scripts em ligações diretas, envia um pedido HEAD para rejeitar ficheiros acima do limite (10 MB para imagens e áudio, 50 MB para vídeo e documentos), e depois transmite o ficheiro através do endpoint fetch-file do proxy com o mesmo limite. Os descarregamentos vazios, demasiado grandes e falhados são eliminados do disco.

Os vídeos grandes de produtos são lentos a enviar a partir da lojaPublicar o vídeo num canal; o bot responde com o seu ID de ficheiro do Telegram para reutilização

Answer vigia as publicações em canais. Quando um vídeo aparece num canal a que o bot pertence, o bot responde com o ID de ficheiro remoto do vídeo. Uma ligação de publicação t.me ou esse ID podem então ser usados como multimédia do produto, e o motor envia-o como ficheiro remoto, de modo que o Telegram o serve do seu próprio armazenamento sem envio por cliente.

Entregar ficheiros digitais pagos dentro da conversaApós o pagamento, enviar cada download pelo fetch protegido, em cache sob um nome mascarado

Quando o carrinho continha produtos descarregáveis e a encomenda é paga online ou é gratuita, do_after_pay() envia cada download desses produtos como documento. Os ficheiros ficam em cache sob um nome derivado do ID do download por shiftUUID(), de modo que o nome guardado não é o ID da loja, e os documentos acima de 50 MB são recusados. Os produtos com acesso limitado mostram os dias restantes, calculados a partir da data de pagamento da encomenda.

As faturas em PDF do endpoint de faturas da loja seguem o mesmo caminho.

Texto da interface em 7 idiomas, editável pelo proprietário da lojaJSON incluído por idioma, sobreposto por substituições do proprietário sincronizadas, com recurso ao inglês

Sete ficheiros de idioma (en, es, pt, ru, pl, kz, ua) com as mesmas 215 chaves são carregados no arranque. __() junta o ficheiro de idioma da conversa com as substituições que o proprietário introduziu na administração (sincronizadas como interface_translations e indexadas como s<number>); se uma chave continuar em falta, tenta de novo com o inglês mais as substituições. O idioma escolhido é guardado por conversa; no primeiro contacto vem do language_code do cliente Telegram quando esse idioma está ativo, caso contrário do valor por defeito da loja.

Como os botões de menu são correspondidos pelo seu texto localizado, os rótulos de uma conversa são resolvidos em paralelo com Promise.all em cada verificação de encaminhamento.

O conteúdo do catálogo e a pesquisa têm de funcionar em cada idiomaTraduções por campo em produtos e termos; a pesquisa corresponde ao título ou às traduções

Produtos, categorias, marcas, campos meta, métodos de envio e de pagamento levam um objeto translations. translate_to() devolve 0 quando a conversa usa o idioma por defeito, pelo que o caso comum salta todas as consultas; caso contrário, get_field_translation() escolhe o campo traduzido e recorre ao original. A pesquisa por título corre LIKE sobre a coluna do título e o JSON de traduções guardado, com um mínimo de 3 carateres, de modo que uma pesquisa em qualquer idioma ativo encontra o produto.

As datas na meta dos produtos e os períodos de acesso são formatados com o idioma da conversa como locale.

Números

do produto
  • Idiomas do bot 7 Inglês, espanhol, português, russo, polaco, cazaque, ucraniano.
  • Métodos de pagamento online 5 Stripe, LiqPay, CoinGate, CryptoBot e Telegram Stars.
  • Caminho de resposta push, sem polling O TDLib mantém uma ligação persistente ao Telegram: cada toque chega de imediato ao bot, sem salto por webhook e sem atraso de polling.