@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.
- package/.env.example +534 -0
- package/LICENSE +21 -0
- package/README.md +431 -0
- package/RELEASE-v2.1.2.md +83 -0
- package/app/config/adminIdentity.js +87 -0
- package/app/config/baileysConfig.js +693 -0
- package/app/config/groupUtils.js +388 -0
- package/app/connection/socketController.js +992 -0
- package/app/controllers/messageController.js +354 -0
- package/app/modules/adminModule/groupCommandHandlers.js +1294 -0
- package/app/modules/adminModule/groupEventHandlers.js +355 -0
- package/app/modules/aiModule/catCommand.js +1006 -0
- package/app/modules/broadcastModule/noticeCommand.js +416 -0
- package/app/modules/gameModule/diceCommand.js +67 -0
- package/app/modules/menuModule/common.js +311 -0
- package/app/modules/menuModule/menus.js +59 -0
- package/app/modules/playModule/playCommand.js +1615 -0
- package/app/modules/quoteModule/quoteCommand.js +851 -0
- package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +786 -0
- package/app/modules/rpgPokemonModule/rpgBattleService.js +2082 -0
- package/app/modules/rpgPokemonModule/rpgBattleService.test.js +760 -0
- package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
- package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +172 -0
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
- package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
- package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
- package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1859 -0
- package/app/modules/rpgPokemonModule/rpgPokemonService.js +6738 -0
- package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
- package/app/modules/statsModule/globalRankingCommand.js +65 -0
- package/app/modules/statsModule/noMessageCommand.js +288 -0
- package/app/modules/statsModule/rankingCommand.js +60 -0
- package/app/modules/statsModule/rankingCommon.js +889 -0
- package/app/modules/stickerModule/addStickerMetadata.js +239 -0
- package/app/modules/stickerModule/convertToWebp.js +390 -0
- package/app/modules/stickerModule/stickerCommand.js +454 -0
- package/app/modules/stickerModule/stickerConvertCommand.js +156 -0
- package/app/modules/stickerModule/stickerTextCommand.js +657 -0
- package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
- package/app/modules/stickerPackModule/autoPackCollectorService.js +284 -0
- package/app/modules/stickerPackModule/semanticReclassificationEngine.js +466 -0
- package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +88 -0
- package/app/modules/stickerPackModule/semanticThemeClusterService.js +571 -0
- package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +449 -0
- package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
- package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +180 -0
- package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +4078 -0
- package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +598 -0
- package/app/modules/stickerPackModule/stickerClassificationService.js +588 -0
- package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +102 -0
- package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +7506 -0
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1095 -0
- package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +108 -0
- package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
- package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +110 -0
- package/app/modules/stickerPackModule/stickerPackItemRepository.js +440 -0
- package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +337 -0
- package/app/modules/stickerPackModule/stickerPackMessageService.js +296 -0
- package/app/modules/stickerPackModule/stickerPackRepository.js +442 -0
- package/app/modules/stickerPackModule/stickerPackService.js +788 -0
- package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +51 -0
- package/app/modules/stickerPackModule/stickerPackUtils.js +97 -0
- package/app/modules/stickerPackModule/stickerStorageService.js +507 -0
- package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +233 -0
- package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +205 -0
- package/app/modules/systemMetricsModule/pingCommand.js +421 -0
- package/app/modules/tiktokModule/tiktokCommand.js +798 -0
- package/app/modules/userModule/userCommand.js +1217 -0
- package/app/modules/waifuPicsModule/waifuPicsCommand.js +177 -0
- package/app/observability/metrics.js +734 -0
- package/app/services/captchaService.js +492 -0
- package/app/services/dbWriteQueue.js +572 -0
- package/app/services/groupMetadataService.js +279 -0
- package/app/services/lidMapService.js +663 -0
- package/app/services/messagePersistenceService.js +56 -0
- package/app/services/newsBroadcastService.js +351 -0
- package/app/services/pokeApiService.js +398 -0
- package/app/services/queueUtils.js +57 -0
- package/app/services/socketState.js +7 -0
- package/app/store/aiPromptStore.js +38 -0
- package/app/store/groupConfigStore.js +58 -0
- package/app/store/premiumUserStore.js +36 -0
- package/app/utils/antiLink/antiLinkModule.js +804 -0
- package/app/utils/http/getImageBufferModule.js +18 -0
- package/app/utils/json/jsonSanitizer.js +113 -0
- package/app/utils/json/jsonSanitizer.test.js +40 -0
- package/app/utils/logger/loggerModule.js +262 -0
- package/app/utils/systemMetrics/systemMetricsModule.js +91 -0
- package/database/index.js +2052 -0
- package/database/init.js +516 -0
- package/database/migrations/20260203_0001_sticker_packs.sql +54 -0
- package/database/migrations/20260210_0003_rpg_pokemon.sql +58 -0
- package/database/migrations/20260210_0004_rpg_shiny_biome.sql +9 -0
- package/database/migrations/20260210_0005_rpg_missions.sql +14 -0
- package/database/migrations/20260210_0006_rpg_world_pokedex_traits.sql +27 -0
- package/database/migrations/20260210_0007_rpg_raid_pvp.sql +56 -0
- package/database/migrations/20260210_0008_rpg_social_system.sql +195 -0
- package/database/migrations/20260211_0009_rpg_social_xp.sql +36 -0
- package/database/migrations/20260222_0010_remove_message_xp.sql +2 -0
- package/database/migrations/20260226_0011_sticker_asset_classification.sql +17 -0
- package/database/migrations/20260226_0012_sticker_pack_engagement.sql +16 -0
- package/database/migrations/20260226_0013_sticker_marketplace_intelligence.sql +19 -0
- package/database/migrations/20260226_0014_sticker_pack_publish_flow.sql +30 -0
- package/database/migrations/20260226_0014_sticker_worker_queues.sql +42 -0
- package/database/migrations/20260226_0015_sticker_auto_pack_curation_integrity.sql +18 -0
- package/database/migrations/20260226_0016_sticker_web_google_auth_persistence.sql +34 -0
- package/database/migrations/20260226_0017_sticker_web_admin_ban.sql +22 -0
- package/database/migrations/20260226_0018_sticker_web_admin_moderator.sql +18 -0
- package/database/migrations/20260227_0019_sticker_classification_v2_signals.sql +12 -0
- package/database/migrations/20260227_0020_semantic_theme_clusters.sql +35 -0
- package/docker-compose.yml +103 -0
- package/ecosystem.prod.config.cjs +35 -0
- package/eslint.config.js +61 -0
- package/index.js +437 -0
- package/ml/clip_classifier/Dockerfile +16 -0
- package/ml/clip_classifier/README.md +120 -0
- package/ml/clip_classifier/adaptive_scoring.py +40 -0
- package/ml/clip_classifier/classifier.py +654 -0
- package/ml/clip_classifier/embedding_store.py +481 -0
- package/ml/clip_classifier/env_loader.py +15 -0
- package/ml/clip_classifier/llm_label_expander.py +144 -0
- package/ml/clip_classifier/main.py +213 -0
- package/ml/clip_classifier/requirements.txt +10 -0
- package/ml/clip_classifier/similarity_engine.py +74 -0
- package/observability/alert-rules.yml +60 -0
- package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
- package/observability/grafana/dashboards/omnizap-overview.json +170 -0
- package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
- package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
- package/observability/loki-config.yml +38 -0
- package/observability/mysql-exporter.cnf +5 -0
- package/observability/mysql-setup.sql +46 -0
- package/observability/prometheus.yml +32 -0
- package/observability/promtail-config.yml +84 -0
- package/package.json +109 -0
- package/public/api-docs/index.html +144 -0
- package/public/css/github-project-panel.css +297 -0
- package/public/css/stickers-admin.css +1272 -0
- package/public/css/styles.css +671 -0
- package/public/index.html +1311 -0
- package/public/js/apps/apiDocsApp.js +310 -0
- package/public/js/apps/createPackApp.js +2069 -0
- package/public/js/apps/homeApp.js +396 -0
- package/public/js/apps/stickersAdminApp.js +1744 -0
- package/public/js/apps/stickersApp.js +4830 -0
- package/public/js/catalog.js +1019 -0
- package/public/js/github-panel/components/CommitList.js +34 -0
- package/public/js/github-panel/components/ErrorState.js +16 -0
- package/public/js/github-panel/components/GithubProjectPanel.js +106 -0
- package/public/js/github-panel/components/ReleaseList.js +38 -0
- package/public/js/github-panel/components/SkeletonPanel.js +22 -0
- package/public/js/github-panel/components/StatCard.js +15 -0
- package/public/js/github-panel/index.js +15 -0
- package/public/js/github-panel/useGithubRepoData.js +154 -0
- package/public/js/github-panel/vendor/react.js +11 -0
- package/public/js/runtime/react-runtime.js +19 -0
- package/public/licenca/index.html +106 -0
- package/public/stickers/admin/index.html +23 -0
- package/public/stickers/create/index.html +47 -0
- package/public/stickers/index.html +48 -0
- package/public/termos-de-uso/index.html +125 -0
- package/scripts/cache-bust.mjs +107 -0
- package/scripts/deploy.sh +458 -0
- package/scripts/github-deploy-notify.mjs +174 -0
- 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
|
+
};
|
package/eslint.config.js
ADDED
|
@@ -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"]
|