@kaikybrofc/omnizap-system 2.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/.env.example +534 -0
  2. package/LICENSE +21 -0
  3. package/README.md +431 -0
  4. package/RELEASE-v2.1.2.md +83 -0
  5. package/app/config/adminIdentity.js +87 -0
  6. package/app/config/baileysConfig.js +693 -0
  7. package/app/config/groupUtils.js +388 -0
  8. package/app/connection/socketController.js +992 -0
  9. package/app/controllers/messageController.js +354 -0
  10. package/app/modules/adminModule/groupCommandHandlers.js +1294 -0
  11. package/app/modules/adminModule/groupEventHandlers.js +355 -0
  12. package/app/modules/aiModule/catCommand.js +1006 -0
  13. package/app/modules/broadcastModule/noticeCommand.js +416 -0
  14. package/app/modules/gameModule/diceCommand.js +67 -0
  15. package/app/modules/menuModule/common.js +311 -0
  16. package/app/modules/menuModule/menus.js +59 -0
  17. package/app/modules/playModule/playCommand.js +1615 -0
  18. package/app/modules/quoteModule/quoteCommand.js +851 -0
  19. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +786 -0
  20. package/app/modules/rpgPokemonModule/rpgBattleService.js +2082 -0
  21. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +760 -0
  22. package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
  23. package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +172 -0
  24. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
  25. package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
  26. package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
  27. package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
  28. package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1859 -0
  29. package/app/modules/rpgPokemonModule/rpgPokemonService.js +6738 -0
  30. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
  31. package/app/modules/statsModule/globalRankingCommand.js +65 -0
  32. package/app/modules/statsModule/noMessageCommand.js +288 -0
  33. package/app/modules/statsModule/rankingCommand.js +60 -0
  34. package/app/modules/statsModule/rankingCommon.js +889 -0
  35. package/app/modules/stickerModule/addStickerMetadata.js +239 -0
  36. package/app/modules/stickerModule/convertToWebp.js +390 -0
  37. package/app/modules/stickerModule/stickerCommand.js +454 -0
  38. package/app/modules/stickerModule/stickerConvertCommand.js +156 -0
  39. package/app/modules/stickerModule/stickerTextCommand.js +657 -0
  40. package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
  41. package/app/modules/stickerPackModule/autoPackCollectorService.js +284 -0
  42. package/app/modules/stickerPackModule/semanticReclassificationEngine.js +466 -0
  43. package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +88 -0
  44. package/app/modules/stickerPackModule/semanticThemeClusterService.js +571 -0
  45. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +449 -0
  46. package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
  47. package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +180 -0
  48. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +4078 -0
  49. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +598 -0
  50. package/app/modules/stickerPackModule/stickerClassificationService.js +588 -0
  51. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +102 -0
  52. package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +7506 -0
  53. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1095 -0
  54. package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +108 -0
  55. package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
  56. package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +110 -0
  57. package/app/modules/stickerPackModule/stickerPackItemRepository.js +440 -0
  58. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +337 -0
  59. package/app/modules/stickerPackModule/stickerPackMessageService.js +296 -0
  60. package/app/modules/stickerPackModule/stickerPackRepository.js +442 -0
  61. package/app/modules/stickerPackModule/stickerPackService.js +788 -0
  62. package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +51 -0
  63. package/app/modules/stickerPackModule/stickerPackUtils.js +97 -0
  64. package/app/modules/stickerPackModule/stickerStorageService.js +507 -0
  65. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +233 -0
  66. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +205 -0
  67. package/app/modules/systemMetricsModule/pingCommand.js +421 -0
  68. package/app/modules/tiktokModule/tiktokCommand.js +798 -0
  69. package/app/modules/userModule/userCommand.js +1217 -0
  70. package/app/modules/waifuPicsModule/waifuPicsCommand.js +177 -0
  71. package/app/observability/metrics.js +734 -0
  72. package/app/services/captchaService.js +492 -0
  73. package/app/services/dbWriteQueue.js +572 -0
  74. package/app/services/groupMetadataService.js +279 -0
  75. package/app/services/lidMapService.js +663 -0
  76. package/app/services/messagePersistenceService.js +56 -0
  77. package/app/services/newsBroadcastService.js +351 -0
  78. package/app/services/pokeApiService.js +398 -0
  79. package/app/services/queueUtils.js +57 -0
  80. package/app/services/socketState.js +7 -0
  81. package/app/store/aiPromptStore.js +38 -0
  82. package/app/store/groupConfigStore.js +58 -0
  83. package/app/store/premiumUserStore.js +36 -0
  84. package/app/utils/antiLink/antiLinkModule.js +804 -0
  85. package/app/utils/http/getImageBufferModule.js +18 -0
  86. package/app/utils/json/jsonSanitizer.js +113 -0
  87. package/app/utils/json/jsonSanitizer.test.js +40 -0
  88. package/app/utils/logger/loggerModule.js +262 -0
  89. package/app/utils/systemMetrics/systemMetricsModule.js +91 -0
  90. package/database/index.js +2052 -0
  91. package/database/init.js +516 -0
  92. package/database/migrations/20260203_0001_sticker_packs.sql +54 -0
  93. package/database/migrations/20260210_0003_rpg_pokemon.sql +58 -0
  94. package/database/migrations/20260210_0004_rpg_shiny_biome.sql +9 -0
  95. package/database/migrations/20260210_0005_rpg_missions.sql +14 -0
  96. package/database/migrations/20260210_0006_rpg_world_pokedex_traits.sql +27 -0
  97. package/database/migrations/20260210_0007_rpg_raid_pvp.sql +56 -0
  98. package/database/migrations/20260210_0008_rpg_social_system.sql +195 -0
  99. package/database/migrations/20260211_0009_rpg_social_xp.sql +36 -0
  100. package/database/migrations/20260222_0010_remove_message_xp.sql +2 -0
  101. package/database/migrations/20260226_0011_sticker_asset_classification.sql +17 -0
  102. package/database/migrations/20260226_0012_sticker_pack_engagement.sql +16 -0
  103. package/database/migrations/20260226_0013_sticker_marketplace_intelligence.sql +19 -0
  104. package/database/migrations/20260226_0014_sticker_pack_publish_flow.sql +30 -0
  105. package/database/migrations/20260226_0014_sticker_worker_queues.sql +42 -0
  106. package/database/migrations/20260226_0015_sticker_auto_pack_curation_integrity.sql +18 -0
  107. package/database/migrations/20260226_0016_sticker_web_google_auth_persistence.sql +34 -0
  108. package/database/migrations/20260226_0017_sticker_web_admin_ban.sql +22 -0
  109. package/database/migrations/20260226_0018_sticker_web_admin_moderator.sql +18 -0
  110. package/database/migrations/20260227_0019_sticker_classification_v2_signals.sql +12 -0
  111. package/database/migrations/20260227_0020_semantic_theme_clusters.sql +35 -0
  112. package/docker-compose.yml +103 -0
  113. package/ecosystem.prod.config.cjs +35 -0
  114. package/eslint.config.js +61 -0
  115. package/index.js +437 -0
  116. package/ml/clip_classifier/Dockerfile +16 -0
  117. package/ml/clip_classifier/README.md +120 -0
  118. package/ml/clip_classifier/adaptive_scoring.py +40 -0
  119. package/ml/clip_classifier/classifier.py +654 -0
  120. package/ml/clip_classifier/embedding_store.py +481 -0
  121. package/ml/clip_classifier/env_loader.py +15 -0
  122. package/ml/clip_classifier/llm_label_expander.py +144 -0
  123. package/ml/clip_classifier/main.py +213 -0
  124. package/ml/clip_classifier/requirements.txt +10 -0
  125. package/ml/clip_classifier/similarity_engine.py +74 -0
  126. package/observability/alert-rules.yml +60 -0
  127. package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
  128. package/observability/grafana/dashboards/omnizap-overview.json +170 -0
  129. package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
  130. package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
  131. package/observability/loki-config.yml +38 -0
  132. package/observability/mysql-exporter.cnf +5 -0
  133. package/observability/mysql-setup.sql +46 -0
  134. package/observability/prometheus.yml +32 -0
  135. package/observability/promtail-config.yml +84 -0
  136. package/package.json +109 -0
  137. package/public/api-docs/index.html +144 -0
  138. package/public/css/github-project-panel.css +297 -0
  139. package/public/css/stickers-admin.css +1272 -0
  140. package/public/css/styles.css +671 -0
  141. package/public/index.html +1311 -0
  142. package/public/js/apps/apiDocsApp.js +310 -0
  143. package/public/js/apps/createPackApp.js +2069 -0
  144. package/public/js/apps/homeApp.js +396 -0
  145. package/public/js/apps/stickersAdminApp.js +1744 -0
  146. package/public/js/apps/stickersApp.js +4830 -0
  147. package/public/js/catalog.js +1019 -0
  148. package/public/js/github-panel/components/CommitList.js +34 -0
  149. package/public/js/github-panel/components/ErrorState.js +16 -0
  150. package/public/js/github-panel/components/GithubProjectPanel.js +106 -0
  151. package/public/js/github-panel/components/ReleaseList.js +38 -0
  152. package/public/js/github-panel/components/SkeletonPanel.js +22 -0
  153. package/public/js/github-panel/components/StatCard.js +15 -0
  154. package/public/js/github-panel/index.js +15 -0
  155. package/public/js/github-panel/useGithubRepoData.js +154 -0
  156. package/public/js/github-panel/vendor/react.js +11 -0
  157. package/public/js/runtime/react-runtime.js +19 -0
  158. package/public/licenca/index.html +106 -0
  159. package/public/stickers/admin/index.html +23 -0
  160. package/public/stickers/create/index.html +47 -0
  161. package/public/stickers/index.html +48 -0
  162. package/public/termos-de-uso/index.html +125 -0
  163. package/scripts/cache-bust.mjs +107 -0
  164. package/scripts/deploy.sh +458 -0
  165. package/scripts/github-deploy-notify.mjs +174 -0
  166. package/scripts/release.sh +129 -0
@@ -0,0 +1,103 @@
1
+ services:
2
+ prometheus:
3
+ image: prom/prometheus:${PROMETHEUS_IMAGE_TAG:-v2.52.0}
4
+ restart: unless-stopped
5
+ command:
6
+ - "--config.file=/etc/prometheus/prometheus.yml"
7
+ - "--storage.tsdb.retention.time=${PROMETHEUS_RETENTION_TIME:-15d}"
8
+ - "--storage.tsdb.retention.size=${PROMETHEUS_RETENTION_SIZE:-20GB}"
9
+ volumes:
10
+ - ${PROMETHEUS_CONFIG_PATH:-./observability/prometheus.yml}:/etc/prometheus/prometheus.yml:ro
11
+ - ${PROMETHEUS_ALERT_RULES_PATH:-./observability/alert-rules.yml}:/etc/prometheus/alert-rules.yml:ro
12
+ - prometheus_data:/prometheus
13
+ ports:
14
+ - "${PROMETHEUS_PORT:-9090}:9090"
15
+ extra_hosts:
16
+ - "host.docker.internal:host-gateway"
17
+
18
+ grafana:
19
+ image: grafana/grafana:${GRAFANA_IMAGE_TAG:-10.4.3}
20
+ restart: unless-stopped
21
+ environment:
22
+ - GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER:-admin}
23
+ - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:-admin}
24
+ - GF_USERS_ALLOW_SIGN_UP=false
25
+ - GF_SERVER_ROOT_URL=${GRAFANA_ROOT_URL:-%(protocol)s://%(domain)s:%(http_port)s/}
26
+ # Opcional: se quiser setar timezone do Grafana
27
+ - GF_DATE_FORMATS_DEFAULT_TIMEZONE=${GRAFANA_TIMEZONE:-America/Boa_Vista}
28
+ volumes:
29
+ - grafana_data:/var/lib/grafana
30
+ - ${GRAFANA_PROVISIONING_PATH:-./observability/grafana/provisioning}:/etc/grafana/provisioning:ro
31
+ - ${GRAFANA_DASHBOARDS_PATH:-./observability/grafana/dashboards}:/var/lib/grafana/dashboards:ro
32
+ ports:
33
+ - "${GRAFANA_PORT:-3003}:3000"
34
+ depends_on:
35
+ - prometheus
36
+ - loki
37
+
38
+ loki:
39
+ image: grafana/loki:${LOKI_IMAGE_TAG:-2.9.4}
40
+ restart: unless-stopped
41
+ command: -config.file=/etc/loki/loki-config.yml
42
+ volumes:
43
+ - ${LOKI_CONFIG_PATH:-./observability/loki-config.yml}:/etc/loki/loki-config.yml:ro
44
+ - loki_data:/loki
45
+ ports:
46
+ - "${LOKI_PORT:-3100}:3100"
47
+
48
+ promtail:
49
+ image: grafana/promtail:${PROMTAIL_IMAGE_TAG:-2.9.4}
50
+ restart: unless-stopped
51
+ command: -config.file=/etc/promtail/config.yml
52
+ volumes:
53
+ - ${PROMTAIL_CONFIG_PATH:-./observability/promtail-config.yml}:/etc/promtail/config.yml:ro
54
+ - ${APP_LOGS_PATH:-./logs}:/var/log/omnizap:ro
55
+ - ${MYSQL_LOGS_PATH:-/var/log/mysql}:/var/log/mysql:ro
56
+ ports:
57
+ - "${PROMTAIL_PORT:-9080}:9080"
58
+ depends_on:
59
+ - loki
60
+ extra_hosts:
61
+ - "host.docker.internal:host-gateway"
62
+
63
+ mysql-exporter:
64
+ image: prom/mysqld-exporter:${MYSQL_EXPORTER_IMAGE_TAG:-v0.15.1}
65
+ restart: unless-stopped
66
+ environment:
67
+ - DATA_SOURCE_NAME=${MYSQL_EXPORTER_DSN:-exporter:exporter@(host.docker.internal:3306)/}
68
+ command:
69
+ - "--config.my-cnf=/etc/mysql-exporter/my.cnf"
70
+ - "--collect.global_status"
71
+ - "--collect.global_variables"
72
+ - "--no-collect.slave_status"
73
+ - "--collect.info_schema.innodb_metrics"
74
+ - "--collect.info_schema.processlist"
75
+ - "--collect.info_schema.tables"
76
+ - "--collect.perf_schema.eventsstatements"
77
+ - "--collect.perf_schema.tableiowaits"
78
+ - "--collect.perf_schema.tablelocks"
79
+ volumes:
80
+ - ${MYSQL_EXPORTER_CNF_PATH:-./observability/mysql-exporter.cnf}:/etc/mysql-exporter/my.cnf:ro
81
+ ports:
82
+ - "${MYSQL_EXPORTER_PORT:-9104}:9104"
83
+ extra_hosts:
84
+ - "host.docker.internal:host-gateway"
85
+
86
+ node-exporter:
87
+ image: prom/node-exporter:${NODE_EXPORTER_IMAGE_TAG:-v1.7.0}
88
+ restart: unless-stopped
89
+ pid: "host"
90
+ command:
91
+ - "--path.rootfs=/host"
92
+ volumes:
93
+ - /:/host:ro,rslave
94
+ ports:
95
+ - "${NODE_EXPORTER_PORT:-9100}:9100"
96
+
97
+ volumes:
98
+ grafana_data:
99
+ name: "${STACK_NAME:-omnizap}_grafana_data"
100
+ prometheus_data:
101
+ name: "${STACK_NAME:-omnizap}_prometheus_data"
102
+ loki_data:
103
+ name: "${STACK_NAME:-omnizap}_loki_data"
@@ -0,0 +1,35 @@
1
+ require('dotenv').config();
2
+
3
+ const appName = process.env.PM2_APP_NAME || 'omnizap-system';
4
+
5
+ module.exports = {
6
+ apps: [
7
+ {
8
+ name: `${appName}-production`,
9
+ script: './index.js',
10
+ cwd: __dirname,
11
+ exec_mode: 'fork',
12
+ instances: 1,
13
+ autorestart: true,
14
+ watch: false,
15
+ max_memory_restart: '3G',
16
+ log_date_format: 'YYYY-MM-DD HH:mm:ss',
17
+ out_file: `logs/${appName}-out.log`,
18
+ error_file: `logs/${appName}-error.log`,
19
+ env: {
20
+ NODE_ENV: 'production',
21
+ COMMAND_PREFIX: '/',
22
+ LOG_LEVEL: 'info',
23
+ DB_LOG_EVERY_QUERY: 'false',
24
+ DB_MONITOR_ENABLED: 'false',
25
+ LID_BACKFILL_ON_START: 'false',
26
+ STICKER_CLASSIFICATION_BACKGROUND_ENABLED: 'true',
27
+ STICKER_REPROCESS_QUEUE_ENABLED: 'true',
28
+ STICKER_AUTO_PACK_BY_TAGS_ENABLED: 'true',
29
+ },
30
+ wait_ready: true,
31
+ listen_timeout: 10000,
32
+ kill_timeout: 5000,
33
+ },
34
+ ],
35
+ };
@@ -0,0 +1,61 @@
1
+ import js from '@eslint/js';
2
+
3
+ export default [
4
+ {
5
+ ignores: [
6
+ 'node_modules/**',
7
+ 'logs/**',
8
+ 'temp/**',
9
+ '.eslintcache',
10
+ '*.log',
11
+ '**/*.min.js',
12
+ 'coverage/**',
13
+ 'dist/**',
14
+ 'build/**',
15
+ ],
16
+ },
17
+ js.configs.recommended,
18
+ {
19
+ files: ['**/*.{js,mjs,cjs}'],
20
+ languageOptions: {
21
+ ecmaVersion: 'latest',
22
+ sourceType: 'module',
23
+ globals: {
24
+ process: 'readonly',
25
+ Buffer: 'readonly',
26
+ console: 'readonly',
27
+ setTimeout: 'readonly',
28
+ clearTimeout: 'readonly',
29
+ setInterval: 'readonly',
30
+ clearInterval: 'readonly',
31
+ setImmediate: 'readonly',
32
+ clearImmediate: 'readonly',
33
+ },
34
+ },
35
+ linterOptions: {
36
+ reportUnusedDisableDirectives: 'warn',
37
+ },
38
+ rules: {
39
+ 'no-var': 'error',
40
+ 'prefer-const': 'warn',
41
+ 'no-unused-vars': [
42
+ 'warn',
43
+ { argsIgnorePattern: '^_', varsIgnorePattern: '^_', ignoreRestSiblings: true },
44
+ ],
45
+ 'no-console': 'off',
46
+ },
47
+ },
48
+ {
49
+ files: ['**/*.cjs'],
50
+ languageOptions: {
51
+ sourceType: 'commonjs',
52
+ globals: {
53
+ module: 'readonly',
54
+ require: 'readonly',
55
+ exports: 'readonly',
56
+ __dirname: 'readonly',
57
+ __filename: 'readonly',
58
+ },
59
+ },
60
+ },
61
+ ];
package/index.js ADDED
@@ -0,0 +1,437 @@
1
+ /**
2
+ * Entry-point (bootstrap) do OmniZap System.
3
+ *
4
+ * Responsabilidades principais:
5
+ * - Inicializar o banco (garantindo DB e tabelas).
6
+ * - Subir servidor de métricas.
7
+ * - Rodar backfill do lid_map (opcional, em background).
8
+ * - Conectar ao WhatsApp (Baileys).
9
+ * - Iniciar serviços auxiliares (ex: broadcast de notícias).
10
+ * - Registrar handlers de shutdown gracioso (SIGINT/SIGTERM) e falhas fatais
11
+ * (uncaughtException/unhandledRejection).
12
+ *
13
+ * Observações:
14
+ * - Este arquivo foi desenhado para ser "production-safe": tem timeouts, shutdown idempotente,
15
+ * e evita process.exit() imediato para não cortar logs/flush e não interromper fechamentos.
16
+ */
17
+
18
+ import 'dotenv/config';
19
+
20
+ import logger from './app/utils/logger/loggerModule.js';
21
+ import { connectToWhatsApp, getActiveSocket } from './app/connection/socketController.js';
22
+ import { backfillLidMapFromMessagesOnce } from './app/services/lidMapService.js';
23
+ import {
24
+ initializeNewsBroadcastService,
25
+ stopNewsBroadcastService,
26
+ } from './app/services/newsBroadcastService.js';
27
+ import initializeDatabase from './database/init.js';
28
+ import { startMetricsServer, stopMetricsServer } from './app/observability/metrics.js';
29
+ import {
30
+ startStickerClassificationBackground,
31
+ stopStickerClassificationBackground,
32
+ } from './app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js';
33
+ import {
34
+ startStickerAutoPackByTagsBackground,
35
+ stopStickerAutoPackByTagsBackground,
36
+ } from './app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js';
37
+ import {
38
+ isStickerWorkerPipelineEnabled,
39
+ startStickerWorkerPipeline,
40
+ stopStickerWorkerPipeline,
41
+ } from './app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js';
42
+
43
+ /**
44
+ * Timeout máximo para inicialização do banco (criar/verificar DB + tabelas).
45
+ * Evita travar o processo em caso de MySQL indisponível ou DNS lento.
46
+ * @type {number}
47
+ */
48
+ const DB_INIT_TIMEOUT_MS = 15000;
49
+
50
+ /**
51
+ * Timeout máximo para conexão inicial do WhatsApp.
52
+ * Dependendo da rede/servidor, a conexão pode demorar, então é maior.
53
+ * @type {number}
54
+ */
55
+ const WHATSAPP_CONNECT_TIMEOUT_MS = 60000;
56
+
57
+ /**
58
+ * Tempo máximo que o shutdown deve aguardar o backfill finalizar.
59
+ * Como backfill é "best-effort", não devemos segurar o shutdown por muito tempo.
60
+ * @type {number}
61
+ */
62
+ const BACKFILL_SHUTDOWN_TIMEOUT_MS = 8000;
63
+
64
+ /**
65
+ * Flag para impedir múltiplos shutdowns concorrentes.
66
+ * @type {boolean}
67
+ */
68
+ let isShuttingDown = false;
69
+
70
+ /**
71
+ * Promise do shutdown em andamento (para idempotência).
72
+ * Se o shutdown for chamado novamente, devolvemos essa mesma promise.
73
+ * @type {Promise<void>|null}
74
+ */
75
+ let shutdownPromise = null;
76
+
77
+ /**
78
+ * Promise do backfill (quando habilitado).
79
+ * Guardamos para poder aguardar com timeout durante o shutdown.
80
+ * @type {Promise<any>|null}
81
+ */
82
+ let backfillPromise = null;
83
+
84
+ /**
85
+ * Erros transitórios conhecidos que não devem derrubar o serviço inteiro.
86
+ * Ex.: limite temporário da API do WhatsApp/Baileys.
87
+ *
88
+ * @param {unknown} reason
89
+ * @returns {boolean}
90
+ */
91
+ const isTransientUnhandledRejection = (reason) => {
92
+ const message =
93
+ reason instanceof Error
94
+ ? String(reason.message || '')
95
+ : typeof reason === 'string'
96
+ ? reason
97
+ : String(reason || '');
98
+
99
+ const normalized = message.trim().toLowerCase();
100
+ if (!normalized) return false;
101
+
102
+ return (
103
+ normalized.includes('rate-overlimit') ||
104
+ normalized.includes('connection closed') ||
105
+ normalized.includes('timed out')
106
+ );
107
+ };
108
+
109
+ /**
110
+ * Executa uma Promise com um timeout.
111
+ *
112
+ * Útil para passos críticos que podem travar:
113
+ * - init do banco
114
+ * - conectar WhatsApp
115
+ * - fechar recursos no shutdown
116
+ *
117
+ * @template T
118
+ * @param {Promise<T>|T} promise - Promise (ou valor) a ser resolvido.
119
+ * @param {number} ms - Tempo máximo em milissegundos.
120
+ * @param {string} label - Nome curto do passo para mensagens de erro.
121
+ * @returns {Promise<T>} Resolve com o valor da Promise, ou rejeita com erro ETIMEOUT.
122
+ */
123
+ const withTimeout = (promise, ms, label) => {
124
+ /** @type {NodeJS.Timeout|undefined} */
125
+ let timeoutId;
126
+
127
+ const timeoutPromise = new Promise((_, reject) => {
128
+ timeoutId = setTimeout(() => {
129
+ const error = new Error(`${label} excedeu ${ms}ms`);
130
+ // Ajuda a filtrar em logs/telemetria
131
+ // @ts-ignore - code é campo comum, mas não está no tipo padrão de Error
132
+ error.code = 'ETIMEOUT';
133
+ reject(error);
134
+ }, ms);
135
+ });
136
+
137
+ return Promise.race([Promise.resolve(promise), timeoutPromise]).finally(() => {
138
+ if (timeoutId) {
139
+ clearTimeout(timeoutId);
140
+ }
141
+ });
142
+ };
143
+
144
+ /**
145
+ * Log helper para erros durante o shutdown, mantendo output consistente.
146
+ *
147
+ * @param {string} context - Texto curto do contexto (ex: "Detalhes do desligamento.").
148
+ * @param {unknown} error - Erro (ou reason) para log.
149
+ * @returns {void}
150
+ */
151
+ const logShutdownError = (context, error) => {
152
+ if (!error) return;
153
+
154
+ if (error instanceof Error) {
155
+ logger.error(context, { error: error.message, stack: error.stack });
156
+ return;
157
+ }
158
+
159
+ logger.error(context, { reason: error });
160
+ };
161
+
162
+ /**
163
+ * Encerra o pool de conexões do MySQL (se existir).
164
+ *
165
+ * Por que o import dinâmico?
166
+ * - Evita problemas de "import cycle" em cenários onde o DB module importa coisas
167
+ * que acabam importando este entrypoint (ou dependências).
168
+ * - Só carrega o módulo no momento do shutdown.
169
+ *
170
+ * @returns {Promise<void>}
171
+ */
172
+ async function closeDatabasePool() {
173
+ try {
174
+ const dbModule = await import('./database/index.js');
175
+ if (typeof dbModule.closePool !== 'function') {
176
+ // O módulo não expõe closePool, então não há o que encerrar aqui.
177
+ return;
178
+ }
179
+
180
+ logger.info('Encerrando pool MySQL...');
181
+ await withTimeout(dbModule.closePool(), 8000, 'closePool');
182
+ logger.info('Pool MySQL encerrado.');
183
+ } catch (error) {
184
+ logger.warn('Falha ao encerrar pool MySQL.', { error: error?.message });
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Inicializa o sistema e seus serviços principais.
190
+ *
191
+ * Fluxo de startup:
192
+ * 1) Inicializa DB (cria/verifica DB/tabelas)
193
+ * 2) Sobe servidor de métricas
194
+ * 3) Inicia backfill (opcional) em background
195
+ * 4) Conecta no WhatsApp
196
+ * 5) Inicia serviços auxiliares (news broadcast)
197
+ * 6) Sinaliza readiness (se estiver rodando sob PM2/cluster com IPC)
198
+ *
199
+ * Em caso de falha: seta exitCode e aciona shutdown gracioso.
200
+ *
201
+ * @returns {Promise<void>}
202
+ */
203
+ async function startApp() {
204
+ try {
205
+ logger.info('Iniciando OmniZap System...');
206
+
207
+ logger.info('Iniciando banco de dados...');
208
+ await withTimeout(initializeDatabase(), DB_INIT_TIMEOUT_MS, 'Inicializacao do banco');
209
+ logger.info('Banco de dados pronto.');
210
+
211
+ logger.info('Inicializando servidor de metricas...');
212
+ startMetricsServer();
213
+ if (isStickerWorkerPipelineEnabled()) {
214
+ startStickerWorkerPipeline();
215
+ } else {
216
+ startStickerClassificationBackground();
217
+ startStickerAutoPackByTagsBackground();
218
+ }
219
+
220
+ // Backfill é opcional, rodando em background.
221
+ const shouldBackfill = process.env.LID_BACKFILL_ON_START !== 'false';
222
+ if (shouldBackfill) {
223
+ const batchSize = Number(process.env.LID_BACKFILL_BATCH) || undefined;
224
+
225
+ logger.info('Iniciando backfill lid_map...');
226
+ backfillPromise = backfillLidMapFromMessagesOnce({ batchSize })
227
+ .then((result) => {
228
+ logger.info('Backfill lid_map concluido.', { batches: result?.batches });
229
+ return result;
230
+ })
231
+ .catch((error) => {
232
+ logger.warn('Backfill lid_map nao concluido.', { error: error.message });
233
+ return null;
234
+ });
235
+ }
236
+
237
+ logger.info('Conectando ao WhatsApp...');
238
+ await withTimeout(connectToWhatsApp(), WHATSAPP_CONNECT_TIMEOUT_MS, 'Conexao WhatsApp');
239
+ logger.info('WhatsApp conectado.');
240
+
241
+ logger.info('Inicializando servico de noticias...');
242
+ await initializeNewsBroadcastService();
243
+ logger.info('Servico de noticias pronto.');
244
+
245
+ logger.info('OmniZap System iniciado com sucesso.');
246
+
247
+ // Compatível com gerenciadores que esperam "ready" via IPC.
248
+ if (process.send) {
249
+ process.send('ready');
250
+ }
251
+ } catch (err) {
252
+ logger.error('Falha ao iniciar o OmniZap System:', { error: err.message, stack: err.stack });
253
+ process.exitCode = 1;
254
+ await shutdown('STARTUP_ERROR', err);
255
+ }
256
+ }
257
+
258
+ startApp();
259
+
260
+ /**
261
+ * Realiza desligamento gracioso do sistema (idempotente).
262
+ *
263
+ * O que fecha:
264
+ * - serviço de notícias (se stop existir)
265
+ * - aguarda backfill (com timeout curto)
266
+ * - encerra socket do WhatsApp
267
+ * - encerra servidor de métricas
268
+ * - encerra pool do MySQL
269
+ *
270
+ * Regras:
271
+ * - Se já estiver desligando, retorna a mesma promise.
272
+ * - Define process.exitCode se ainda não estiver definido.
273
+ * - Ao finalizar, encerra o processo explicitamente para permitir restart limpo no PM2.
274
+ *
275
+ * @param {string} signal - Origem do shutdown (SIGINT, SIGTERM, uncaughtException, etc).
276
+ * @param {unknown} [error] - Erro associado (se houver).
277
+ * @returns {Promise<void>}
278
+ */
279
+ async function shutdown(signal, error) {
280
+ if (isShuttingDown) {
281
+ return shutdownPromise;
282
+ }
283
+ isShuttingDown = true;
284
+
285
+ if (process.exitCode === undefined || process.exitCode === null) {
286
+ process.exitCode = error ? 1 : 0;
287
+ }
288
+
289
+ logger.warn(`${signal} recebido. Iniciando desligamento gracioso...`);
290
+ logShutdownError('Detalhes do desligamento.', error);
291
+
292
+ shutdownPromise = (async () => {
293
+ // 1) Serviços com timers/intervals (news broadcast)
294
+ try {
295
+ if (typeof stopNewsBroadcastService === 'function') {
296
+ logger.info('Encerrando servico de noticias...');
297
+ stopNewsBroadcastService();
298
+ logger.info('Servico de noticias encerrado.');
299
+ }
300
+ } catch (stopError) {
301
+ logger.warn('Falha ao encerrar servico de noticias.', { error: stopError.message });
302
+ }
303
+
304
+ // 2) Esperar backfill (best-effort) com timeout
305
+ if (backfillPromise) {
306
+ try {
307
+ logger.info('Aguardando backfill lid_map...');
308
+ await withTimeout(backfillPromise, BACKFILL_SHUTDOWN_TIMEOUT_MS, 'Backfill lid_map');
309
+ logger.info('Backfill lid_map finalizado.');
310
+ } catch (backfillError) {
311
+ logger.warn('Backfill lid_map nao finalizou antes do shutdown.', {
312
+ error: backfillError.message,
313
+ });
314
+ }
315
+ }
316
+
317
+ // 3) Encerrar conexão WhatsApp
318
+ const sock = getActiveSocket();
319
+ if (sock) {
320
+ try {
321
+ logger.info('Encerrando conexão do WhatsApp...');
322
+ await withTimeout(sock.end(), 8000, 'Encerramento WhatsApp');
323
+ logger.info('Conexao do WhatsApp encerrada.');
324
+ } catch (sockError) {
325
+ logger.error('Erro ao encerrar a conexão do WhatsApp:', {
326
+ error: sockError.message,
327
+ stack: sockError.stack,
328
+ });
329
+ }
330
+ }
331
+
332
+ // 4) Encerrar servidor de métricas
333
+ if (typeof stopMetricsServer === 'function') {
334
+ try {
335
+ logger.info('Encerrando servidor de metricas...');
336
+ await withTimeout(stopMetricsServer(), 8000, 'Encerramento metricas');
337
+ logger.info('Servidor de metricas encerrado.');
338
+ } catch (metricsError) {
339
+ logger.warn('Falha ao encerrar servidor de metricas.', { error: metricsError.message });
340
+ }
341
+ }
342
+
343
+ // 4.1) Encerrar worker de classificação de stickers
344
+ try {
345
+ if (isStickerWorkerPipelineEnabled()) {
346
+ stopStickerWorkerPipeline();
347
+ } else {
348
+ stopStickerClassificationBackground();
349
+ }
350
+ } catch (workerError) {
351
+ logger.warn('Falha ao encerrar worker de classificação de stickers.', {
352
+ error: workerError?.message,
353
+ });
354
+ }
355
+
356
+ try {
357
+ if (!isStickerWorkerPipelineEnabled()) {
358
+ stopStickerAutoPackByTagsBackground();
359
+ }
360
+ } catch (workerError) {
361
+ logger.warn('Falha ao encerrar worker de auto-pack por tags.', {
362
+ error: workerError?.message,
363
+ });
364
+ }
365
+
366
+ // 5) Encerrar MySQL pool
367
+ await closeDatabasePool();
368
+
369
+ logger.info('OmniZap System desligado.');
370
+
371
+ const exitCode = Number(process.exitCode ?? (error ? 1 : 0));
372
+ logger.info('Encerrando processo Node.', { exitCode, signal });
373
+ // Força término para evitar processo "online" sem servidor HTTP ativo.
374
+ process.exit(exitCode);
375
+ })();
376
+
377
+ return shutdownPromise;
378
+ }
379
+
380
+ /**
381
+ * Handler para interrupção no terminal (Ctrl+C).
382
+ * @returns {void}
383
+ */
384
+ process.on('SIGINT', () => {
385
+ void shutdown('SIGINT');
386
+ });
387
+
388
+ /**
389
+ * Handler para encerramento solicitado pelo sistema (ex: container/PM2).
390
+ * @returns {void}
391
+ */
392
+ process.on('SIGTERM', () => {
393
+ void shutdown('SIGTERM');
394
+ });
395
+
396
+ /**
397
+ * Handler para exceções não capturadas (fatal).
398
+ * - seta exitCode=1
399
+ * - inicia shutdown gracioso para tentar fechar recursos antes de morrer
400
+ *
401
+ * @param {Error} err
402
+ * @returns {void}
403
+ */
404
+ process.on('uncaughtException', (err) => {
405
+ logger.error('Exceção não capturada:', { error: err.message, stack: err.stack });
406
+ process.exitCode = 1;
407
+ void shutdown('uncaughtException', err);
408
+ });
409
+
410
+ /**
411
+ * Handler para rejeições de promise sem catch (fatal).
412
+ * - loga reason
413
+ * - seta exitCode=1
414
+ * - inicia shutdown gracioso
415
+ *
416
+ * Observação: o parâmetro `promise` é incluído apenas para debug, mas geralmente não é útil.
417
+ *
418
+ * @param {unknown} reason
419
+ * @param {Promise<unknown>} promise
420
+ * @returns {void}
421
+ */
422
+ process.on('unhandledRejection', (reason, promise) => {
423
+ if (isTransientUnhandledRejection(reason)) {
424
+ logger.warn('Rejeição de promessa transitória ignorada para manter disponibilidade.', {
425
+ reason: reason instanceof Error ? reason.message : String(reason || ''),
426
+ });
427
+ return;
428
+ }
429
+
430
+ if (reason instanceof Error) {
431
+ logger.error('Rejeição de promessa não tratada:', { error: reason.message, stack: reason.stack });
432
+ } else {
433
+ logger.error('Rejeição de promessa não tratada:', { reason, promise });
434
+ }
435
+ process.exitCode = 1;
436
+ void shutdown('unhandledRejection', reason);
437
+ });
@@ -0,0 +1,16 @@
1
+ FROM python:3.11-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1 \
4
+ PYTHONUNBUFFERED=1 \
5
+ PIP_NO_CACHE_DIR=1
6
+
7
+ WORKDIR /app
8
+
9
+ COPY requirements.txt .
10
+ RUN pip install -r requirements.txt
11
+
12
+ COPY . .
13
+
14
+ EXPOSE 8008
15
+
16
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8008"]