Chatbot
1. Descripción general
chatbot es un microservicio batch (según el pom.xml, "Chatbot for marketplaces") que automatiza las respuestas del chat de vendedor de Shopee para las cinco tiendas regionales de Hawkers (Malasia, Tailandia, Singapur, Filipinas y Vietnam). No expone ninguna API HTTP: es una aplicación Spring Boot basada en dos CommandLineRunner que se ejecutan secuencialmente y terminan, desplegada como un CronJob de Kubernetes que se dispara cada 10 minutos.
El flujo tiene dos fases: primero recupera las conversaciones nuevas de cada tienda Shopee y las persiste en base de datos; después, para cada conversación aún sin responder, envía un mensaje de saludo automático (con plantilla distinta según horario laboral o fin de semana/nocturno) y marca la conversación como no leída en Shopee para que un agente humano la retome. Dentro del ecosistema Hawkers, actúa como capa de automatización de primer contacto sobre el canal de mensajería de Shopee.
2. Información técnica
| Propiedad | Valor |
|---|---|
artifactId | chatbot |
groupId | com.hawkersco |
version | 1.0.25 |
| Java | 25 |
| Spring Boot | 4.0.6 |
| Tipo de artefacto | JAR ejecutable (imagen de contenedor vía Jib) |
| Módulos | Proyecto único (no multi-módulo) |
3. Arquitectura y diseño
Estructura del proyecto:
com.hawkersco.chatbot
├── ChatbotApplication.java # @SpringBootApplication + @EnableJpaRepositories + @EnableConfigurationProperties
├── ChatbotShopeeGetMessagerRunner.java # CommandLineRunner, orden 1: obtiene conversaciones nuevas de Shopee
├── ChatbotShopeeSendMessagesRunner.java # CommandLineRunner, orden 2: responde conversaciones pendientes y las marca como no leídas
├── config/
│ ├── ChatbotConfig.java # Declara manualmente los @Bean de ChatbotUtils y ChatConversationService (logistics-commons)
│ ├── MessageProperties.java # @ConfigurationProperties("message") — plantillas de saludo (weekday/weekend)
│ └── ShopeeApiProperties.java # @ConfigurationProperties("shopee.api") — partnerId/Key, shopId por región, rutas de API
└── utils/
└── ChatbotUtils.java # Obtención de token vía pi-generate-credentials-client, firma HMAC-SHA256
Flujo principal (dos runners ordenados, por tienda)
sequenceDiagram
participant CronJob as CronJob K8s (cada 10 min)
participant R1 as ChatbotShopeeGetMessagerRunner (orden 1)
participant Cred as pi-generate-credentials-client
participant Shopee as API Shopee
participant DB as PostgreSQL (logistics.chat_conversation)
participant R2 as ChatbotShopeeSendMessagesRunner (orden 2)
CronJob->>R1: run()
loop por cada tienda (MY, TH, SG, PH, VN)
R1->>Cred: getToken("SHOPEE_<región>")
R1->>R1: firma HMAC-SHA256 (partnerId+path+timestamp+token+shopId)
R1->>Shopee: GET get_conversation_list
Shopee-->>R1: lista de conversaciones
R1->>DB: persiste conversaciones nuevas (replied=false)
end
CronJob->>R2: run()
loop por cada tienda
R2->>DB: findByIsReplied(false, "SHOPEE_<región>")
loop por cada conversación pendiente
R2->>R2: elige plantilla (weekday/weekend según hora Europe/Madrid)
R2->>Shopee: POST send_message (saludo automático)
R2->>DB: marca conversación como respondida
R2->>Shopee: POST unread_conversation (para que un agente humano la retome)
end
end
R2->>R2: System.exit(SpringApplication.exit(context))
ChatbotApplication usa @EnableJpaRepositories("com.hawkersco.logisticscommons.repository") (repositorios JPA externos, en logistics-commons) y @EnableConfigurationProperties({ShopeeApiProperties.class, MessageProperties.class}) para vincular los records de configuración tipada. ChatbotConfig declara manualmente ChatbotUtils y ChatConversationService como beans (mismo patrón que bradery-create-db, ya que los servicios de logistics-commons no se auto-detectan por @ComponentScan).
ChatbotShopeeSendMessagesRunner.run() termina con System.exit(SpringApplication.exit(context)) — a diferencia de la mayoría de runners batch del ecosistema, fuerza explícitamente la finalización del proceso con el código de salida devuelto por Spring, en lugar de dejar que la JVM termine de forma natural tras el main.
4. Dependencias principales
| Dependencia | Versión | Propósito |
|---|---|---|
spring-boot-starter | (gestionada SB4) | Base de Spring Boot (contexto, CommandLineRunner, @ConfigurationProperties) |
spring-boot-starter-data-jpa | (gestionada SB4) | Acceso JPA/Hibernate a las entidades de logistics-commons |
spring-web | (gestionada SB4) | Soporte HTTP para los clientes @HttpExchange inyectados |
com.hawkersco:logistics-commons | 1.0.25-SNAPSHOT | Entidad ChatConversation y ChatConversationService |
com.hawkersco:pi-generate-credentials-client | 1.0.25-SNAPSHOT | Cliente HTTP para obtener el token de acceso Shopee por tienda |
com.hawkersco:pi-function-commons | 1.0.25-SNAPSHOT | Utilidad DateUtils |
com.hawkersco:shopee-client | 1.0.25-SNAPSHOT | Cliente HTTP para todas las llamadas a la API de Shopee |
spring-boot-starter-test | (gestionada SB4) | Testing (scope test) |
CLAUDE.md menciona una dependencia slack.*/notificaciones Slack (grupo de propiedades slack.* presente en application.properties), pero no existe ninguna dependencia slack-client en el pom.xml ni ningún código que use dichas propiedades (confirmado por búsqueda en el código fuente) — ver detalle de seguridad en la sección 13. CLAUDE.md también fija las librerías internas en 1.0.17; el pom.xml real usa 1.0.25-SNAPSHOT.
5. API / Endpoints
No aplica a este proyecto. chatbot es un batch CommandLineRunner sin servidor HTTP ni controladores REST.
6. Integraciones externas
| Sistema | Protocolo | Dirección | Descripción |
|---|---|---|---|
pi-generate-credentials (servicio interno) | HTTP REST | Saliente | Obtiene el access token de Shopee para cada tienda (SHOPEE_MY, SHOPEE_TH, ...) |
| API Shopee Open Platform | HTTP REST, firma HMAC-SHA256 | Saliente | get_conversation_list, send_message, unread_conversation |
PostgreSQL (logistics, vía logistics-commons) | JDBC | Saliente | Persistencia de conversaciones (ChatConversation) |
Esquema de firma HMAC (ChatbotUtils.hmacDigest, idéntico patrón al de shopee-client):
baseString = partnerId + apiPath + timestamp + accessToken + shopId
signature = HMAC-SHA256(baseString, partnerKey) // hex, zero-padded a 64 caracteres
La firma se recalcula de forma independiente antes de cada llamada (obtención de conversaciones, envío de mensaje, marcado como no leído), ya que cada endpoint tiene su propia ruta y por tanto su propio baseString.
Reglas de negocio de horario (isWeekendOrNightHours, zona horaria Europe/Madrid): se considera "fuera de horario" (plantilla weekend) todo el domingo, el sábado a partir de las 05:30 y el lunes antes de la 01:00; el resto del tiempo se usa la plantilla weekday.
7. Configuración
El proyecto usa dos perfiles: application.properties (desarrollo, con valores reales) y application-pro.properties (producción, con placeholders ${VAR} inyectados por Kubernetes). El Jenkinsfile sustituye el primero por el segundo antes de empaquetar.
Grupos de propiedades
| Propiedad | Descripción | Valor en producción |
|---|---|---|
spring.datasource.url/username/password | Conexión PostgreSQL de logística | ${dbLogisitcsUrl} / ${dbLogisitcsUsername} / ${dbLogisitcsPassword} (sic, con typo en el nombre de variable) |
shopee.api.host | URL base de la API Shopee (https://partner.shopeemobile.com) | ${shopeeApiHost} |
shopee.api.partnerId | Partner ID de la integración Shopee | ${shopeeApiPartnerId} |
shopee.api.partnerKey | Clave secreta para la firma HMAC | ${shopeeApiKey} |
shopee.api.shopId.{my,th,sg,ph,vn} | ID de tienda Shopee por región | ${shopeeApiShopIdMy} (y análogos) |
shopee.api.path.conversation.get/reply/mark.unread | Rutas de la API de chat de Shopee | (fijas, no varían por entorno) |
credentials-client.api.host | Host del servicio pi-generate-credentials | ${credentialsClientApiHost} |
message.weekday / message.weekend | Plantillas de saludo automático | (mismo texto en ambos perfiles) |
slack.client.url / slack.auth.token / slack.channel.id | Configuración de Slack — no usada por ningún código actual | ${slackClientUrl} / ${slackAuthToken} / ${slackChannelId} |
⚠️ Alerta de seguridad crítica: El fichero
src/main/resources/application.properties(perfil de desarrollo, versionado en el repositorio) contiene secretos reales en texto plano, incluyendo un token de bot de Slack (slack.auth.token, con prefijoxoxb-...), la clave de partner de Shopee (shopee.api.partnerKey) y la contraseña de la base de datos PostgreSQL. Ninguno de estos valores se reproduce en este documento. Dado que el token de Slack concede capacidad de publicar en el workspace de Slack de la organización en nombre del bot, debe rotarse de inmediato con independencia de que el código no lo esté usando actualmente (ver sección 13).
Variables de entorno (perfil de producción)
| Variable | Propiedad mapeada |
|---|---|
dbLogisitcsUrl / Username / Password | spring.datasource.* (nombres con el typo "Logisitcs", ver sección 13) |
shopeeApiHost | shopee.api.host |
shopeeApiPartnerId | shopee.api.partnerId |
shopeeApiKey | shopee.api.partnerKey |
shopeeApiShopIdMy/Th/Sg/Ph/Vn | shopee.api.shopId.{my,th,sg,ph,vn} |
credentialsClientApiHost | credentials-client.api.host |
slackClientUrl / slackAuthToken / slackChannelId | slack.* (sin uso en el código, ver sección 13) |
8. Persistencia
Base de datos: PostgreSQL (logistics, instancia noctua-instance.hawkersco.net), accedida vía JPA/Hibernate a través de las entidades de logistics-commons.
Entidad principal:
| Entidad | Rol |
|---|---|
ChatConversation | Conversación de chat de Shopee: idConversation, idClient, replied, dtCreated, dtReplied, dsReply, marketplace (identificador de tienda, p. ej. SHOPEE_MY) |
No se han encontrado migraciones Flyway/Liquibase en este proyecto (spring.jpa.hibernate.ddl-auto=none); el esquema se gestiona externamente, compartido con otros proyectos que usan logistics-commons.
9. Procesos programados y mensajería
No hay @Scheduled en el código: la periodicidad se gestiona íntegramente vía Kubernetes. k8s/cronjob.yaml define un CronJob con expresión */10 * * * * (cada 10 minutos), concurrencyPolicy: Forbid (no permite solapar ejecuciones si la anterior aún no ha terminado), activeDeadlineSeconds: 3600 y restartPolicy: OnFailure.
Dentro de cada ejecución, ChatbotShopeeGetMessagerRunner (orden 1) y ChatbotShopeeSendMessagesRunner (orden 2) se ejecutan secuencialmente al arrancar el contexto de Spring, iterando ambos sobre las cinco tiendas regionales.
10. Ejecución en local
Requisitos previos
- Java 25
- Maven 3.x
- Acceso a la base de datos PostgreSQL de logística
- Acceso al servicio
pi-generate-credentials(o acredentials-client.api.host=http://localhost:8081si se ejecuta localmente, según el valor por defecto enapplication.properties) - Credenciales válidas de partner de Shopee para las tiendas regionales
Comandos
# Build sin tests (como en CI)
./mvnw -B -DskipTests clean install
# Build con tests
./mvnw clean install
# Ejecutar tests
./mvnw test
# Build de imagen Docker
docker build -t europe-west3-docker.pkg.dev/pi-saldum/pi-repo/chatbot:<version> .
Al no tener servidor HTTP, no hay endpoint de health; la verificación de correcta ejecución se hace revisando los logs (Chatbot GET Conversation Shopee - END, Chatbot Send Message & mark as unread Shopee - END) o el estado del Job generado por el CronJob en Kubernetes.
11. Despliegue
Se despliega como imagen de contenedor en GKE, orquestada por un CronJob, con el mismo patrón de pipeline que bradery-create-db:
- Checkout — descarga el código.
- Build & Push — sustituye
application.propertiesporapplication-pro.properties, compila conmvn clean package jib:build -DskipTests -U -Dimage.tag=${BUILD_NUMBER}y publica eneurope-west3-docker.pkg.dev/pi-saldum/pi-repo/chatbot:${BUILD_NUMBER}. - Deploy to GKE — obtiene credenciales del clúster
pi-cluster-hw(zonaeurope-west3-a), elimina elCronJobexistente y aplicak8s/cronjob.yamlen el namespacepi.
Imagen base de runtime (Jib): eclipse-temurin:25-jre, consistente con Java 25.
CLAUDE.md documenta un pipeline de 7 etapas (Build → KICS → SonarQube → Test → Push → Deployment → Clean) usando scripts en jenkins/scripts/ (mvn.sh, test.sh, clean.sh, deployment.sh), que no se corresponde con el Jenkinsfile actual del repositorio (tres etapas: Checkout, Build & Push, Deploy to GKE, sin scripts externos ni KICS/SonarQube). Se documenta el Jenkinsfile realmente presente.
Job de Jenkins:
https://jenkins-pi.hawkersco.net/job/chatbot/
12. Manejo de errores y logging
Ambos runners usan java.util.logging.Logger. ChatbotShopeeGetMessagerRunner.run() y ChatbotShopeeSendMessagesRunner.run() envuelven todo su bucle principal (todas las tiendas) en un único try/catch(Exception) que registra el error (LOGGER.log(Level.SEVERE, ...)) y continúa sin relanzar — un fallo al procesar una tienda concreta (p. ej. token inválido para SHOPEE_TH) detiene el procesamiento de todas las tiendas restantes en esa ejecución del runner, ya que el catch envuelve el bucle completo, no cada iteración por separado.
ChatbotUtils.getTokenShopeeApiCredentials devuelve una cadena vacía si la respuesta de credenciales no es 2xx o no tiene body, sin lanzar excepción — una petición Shopee posterior con token vacío probablemente fallará con un error HTTP del lado de Shopee en lugar de fallar explícitamente en el punto de obtención del token, mismo patrón observado en otros clientes del ecosistema con fallo silencioso de autenticación.
No hay configuración de logback específica; los logs usan el formato por defecto de java.util.logging.
13. Notas y consideraciones
-
⚠️ Secretos reales expuestos en el repositorio: ver alerta detallada en la sección 7. Es el hallazgo más crítico de este proyecto — un token de bot de Slack activo (
xoxb-...), la clave de partner de Shopee y la contraseña de la base de datos están en texto plano ensrc/main/resources/application.properties, versionado en el control de código fuente. Se recomienda rotar todas estas credenciales de inmediato y migrar el perfil de desarrollo local al mismo patrón de variables de entorno que ya usaapplication-pro.properties. -
Configuración de Slack completamente sin usar: Las propiedades
slack.client.url,slack.auth.tokenyslack.channel.id(incluyendo el token real mencionado arriba) no son leídas por ningún componente del código fuente actual — no hay ningún@Value/@ConfigurationPropertiesque las vincule ni ningún cliente Slack en las dependencias delpom.xml. Es probable que sean un remanente de un plan de notificaciones de errores por Slack nunca implementado, o de una integración retirada. Su presencia no solo es deuda técnica sino que agrava el problema de seguridad: un secreto vivo se mantiene expuesto sin ningún propósito funcional. -
Typo consistente "Logisitcs" en las variables de entorno de producción: Tanto
application-pro.propertiescomok8s/cronjob.yamlusan de forma consistentedbLogisitcsUrl/dbLogisitcsUsername/dbLogisitcsPassword(con las letras "cs" y "it" intercambiadas respecto a "Logistics"). Al ser consistente entre ambos ficheros, no rompe el despliegue, pero cualquier nuevo desarrollador que busquedbLogisticsUrl(ortografía correcta, usada enbradery-create-db) no lo encontrará aquí. -
Manejo de errores a nivel de todas las tiendas, no por tienda: Ver detalle en la sección 12. Un fallo aislado en una tienda regional puede impedir que se procesen las demás en la misma ejecución del runner — dado que el
CronJobse repite cada 10 minutos, el impacto práctico de un fallo puntual es limitado, pero durante una incidencia prolongada en una tienda (p. ej. credenciales caducadas) ninguna tienda recibiría respuestas automáticas hasta resolver el problema de la tienda afectada. -
Comentario de Javadoc desactualizado en
ChatbotUtils: El Javadoc degetTokenShopeeApiCredentialsdescribe el parámetropiGenerateCredentialsClientcomo "Feign client for the credentials API", pese a quepi-generate-credentials-clientya migró de Feign a@HttpExchange/RestClient(confirmado en su propia documentación). Comentario heredado no actualizado tras la migración. -
System.exit()explícito solo en el segundo runner:ChatbotShopeeSendMessagesRunnerfuerza la terminación del proceso conSystem.exit(SpringApplication.exit(context)), mientras queChatbotShopeeGetMessagerRunnerno lo hace — dado que ambos sonCommandLineRunnerdentro de la misma aplicación y se ejecutan en el mismo arranque, el efecto práctico es que el proceso termina de forma explícita al finalizar el segundo runner (el que se ejecuta en último lugar), lo cual es redundante pero no incorrecto. -
Sin tests funcionales:
ChatbotApplicationTestssolo contiene el testcontextLoads()generado por defecto; no hay ninguna prueba sobre la lógica de firma HMAC, el cálculo de horario laboral ni la persistencia de conversaciones.