@omnizap-system/omnizap 2.6.1 → 2.6.3

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 (172) hide show
  1. package/.env.example +78 -9
  2. package/.github/workflows/ci.yml +3 -3
  3. package/.github/workflows/security-runner-hardening.yml +1 -1
  4. package/.github/workflows/security-zap-full-scan.yml +1 -0
  5. package/app/config/index.js +6 -0
  6. package/app/configParts/adminIdentity.js +36 -7
  7. package/app/configParts/baileysConfig.js +343 -56
  8. package/app/configParts/groupUtils.js +226 -0
  9. package/app/configParts/loggerConfig.js +185 -0
  10. package/app/configParts/messagePersistenceService.js +307 -5
  11. package/app/configParts/sessionConfig.js +242 -0
  12. package/app/connection/baileysCompatibility.test.js +10 -1
  13. package/app/connection/baileysDbAuthState.js +205 -9
  14. package/app/connection/baileysLibsignalPatch.js +210 -0
  15. package/app/connection/groupOwnerWriteStateResolver.js +141 -0
  16. package/app/connection/socketController.js +694 -123
  17. package/app/connection/socketController.multiSession.test.js +128 -0
  18. package/app/controllers/messageController.js +1 -1
  19. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  20. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  21. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  22. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +96 -4
  23. package/app/controllers/messageProcessingPipeline.js +90 -9
  24. package/app/controllers/messageProcessingPipeline.test.js +202 -0
  25. package/app/modules/adminModule/AGENT.md +1 -1
  26. package/app/modules/adminModule/commandConfig.json +3318 -1347
  27. package/app/modules/adminModule/groupCommandHandlers.js +856 -14
  28. package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
  29. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  30. package/app/modules/aiModule/AGENT.md +47 -30
  31. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  32. package/app/modules/aiModule/catCommand.js +132 -25
  33. package/app/modules/aiModule/commandConfig.json +114 -28
  34. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  35. package/app/modules/gameModule/AGENT.md +1 -1
  36. package/app/modules/gameModule/commandConfig.json +29 -0
  37. package/app/modules/menuModule/AGENT.md +1 -1
  38. package/app/modules/menuModule/commandConfig.json +45 -10
  39. package/app/modules/menuModule/menuCatalogService.js +190 -0
  40. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  41. package/app/modules/menuModule/menuDynamicService.js +511 -0
  42. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  43. package/app/modules/menuModule/menus.js +36 -5
  44. package/app/modules/playModule/AGENT.md +10 -5
  45. package/app/modules/playModule/commandConfig.json +74 -16
  46. package/app/modules/playModule/playCommandConstants.js +13 -7
  47. package/app/modules/playModule/playCommandCore.js +4 -6
  48. package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
  49. package/app/modules/playModule/playConfigRuntime.js +5 -6
  50. package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
  51. package/app/modules/quoteModule/AGENT.md +1 -1
  52. package/app/modules/quoteModule/commandConfig.json +29 -0
  53. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  54. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  55. package/app/modules/statsModule/AGENT.md +1 -1
  56. package/app/modules/statsModule/commandConfig.json +58 -0
  57. package/app/modules/stickerModule/AGENT.md +1 -1
  58. package/app/modules/stickerModule/commandConfig.json +145 -0
  59. package/app/modules/stickerPackModule/AGENT.md +1 -1
  60. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  61. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  62. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
  63. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
  64. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  65. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  66. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  67. package/app/modules/tiktokModule/AGENT.md +1 -1
  68. package/app/modules/tiktokModule/commandConfig.json +29 -0
  69. package/app/modules/userModule/AGENT.md +1 -1
  70. package/app/modules/userModule/commandConfig.json +29 -0
  71. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  72. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  73. package/app/observability/metrics.js +136 -0
  74. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  75. package/app/services/ai/geminiService.js +131 -7
  76. package/app/services/ai/geminiService.test.js +59 -2
  77. package/app/services/ai/moduleAiHelpCoreService.js +33 -4
  78. package/app/services/group/groupMetadataService.js +24 -1
  79. package/app/services/infra/dbWriteQueue.js +51 -21
  80. package/app/services/messaging/newsBroadcastService.js +843 -27
  81. package/app/services/multiSession/assignmentBalancerService.js +452 -0
  82. package/app/services/multiSession/groupOwnershipRepository.js +346 -0
  83. package/app/services/multiSession/groupOwnershipService.js +809 -0
  84. package/app/services/multiSession/groupOwnershipService.test.js +317 -0
  85. package/app/services/multiSession/sessionRegistryService.js +239 -0
  86. package/app/store/aiPromptStore.js +36 -19
  87. package/app/store/groupConfigStore.js +41 -5
  88. package/app/store/premiumUserStore.js +21 -7
  89. package/app/utils/antiLink/antiLinkModule.js +391 -25
  90. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  91. package/database/index.js +6 -0
  92. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  93. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  94. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  95. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  96. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  97. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  98. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  99. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  100. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  101. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  102. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  103. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  104. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  105. package/database/schema.sql +102 -1
  106. package/docker-compose.yml +4 -1
  107. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  108. package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
  109. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  110. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  111. package/docs/security/omnizap-static-security-headers.conf +25 -0
  112. package/ecosystem.prod.config.cjs +31 -11
  113. package/index.js +52 -18
  114. package/observability/alert-rules.yml +20 -0
  115. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  116. package/observability/mysql-setup.sql +4 -4
  117. package/observability/system-admin-observability.md +26 -0
  118. package/package.json +14 -6
  119. package/public/comandos/commands-catalog.json +2253 -78
  120. package/public/css/payments-react.css +478 -0
  121. package/public/js/apps/commandsReactApp.js +267 -87
  122. package/public/js/apps/createPackApp.js +3 -3
  123. package/public/js/apps/homeReactApp.js +2 -2
  124. package/public/js/apps/paymentsCancelReactApp.js +45 -0
  125. package/public/js/apps/paymentsReactApp.js +399 -0
  126. package/public/js/apps/paymentsSuccessReactApp.js +148 -0
  127. package/public/js/apps/stickersApp.js +255 -103
  128. package/public/js/apps/termsReactApp.js +57 -8
  129. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  130. package/public/js/apps/userReactApp.js +96 -47
  131. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  132. package/public/pages/pagamentos-cancelado.html +21 -0
  133. package/public/pages/pagamentos-sucesso.html +21 -0
  134. package/public/pages/pagamentos.html +30 -0
  135. package/public/pages/politica-de-privacidade.html +1 -1
  136. package/public/pages/stickers.html +5 -5
  137. package/public/pages/termos-de-uso-texto-integral.html +1 -1
  138. package/public/pages/termos-de-uso.html +1 -1
  139. package/public/pages/user-password-reset.html +3 -4
  140. package/public/pages/user-systemadm.html +8 -462
  141. package/public/pages/user.html +1 -1
  142. package/scripts/clear-whatsapp-session.sh +123 -0
  143. package/scripts/core-ai-mode.mjs +163 -0
  144. package/scripts/deploy.sh +13 -0
  145. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  146. package/scripts/generate-commands-catalog.mjs +155 -0
  147. package/scripts/new-whatsapp-session.sh +564 -0
  148. package/scripts/security-web-surface-check.mjs +218 -0
  149. package/server/controllers/admin/adminPanelHandlers.js +253 -3
  150. package/server/controllers/admin/systemAdminController.js +254 -0
  151. package/server/controllers/payments/paymentsController.js +731 -0
  152. package/server/controllers/sticker/stickerCatalogController.js +9 -23
  153. package/server/controllers/system/contactController.js +9 -17
  154. package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
  155. package/server/controllers/system/systemController.js +228 -1
  156. package/server/controllers/userController.js +6 -0
  157. package/server/email/emailAutomationRuntime.js +36 -1
  158. package/server/email/emailAutomationService.js +42 -1
  159. package/server/email/emailTemplateService.js +140 -33
  160. package/server/http/httpRequestUtils.js +18 -14
  161. package/server/http/httpServer.js +8 -4
  162. package/server/middleware/securityHeaders.js +35 -3
  163. package/server/routes/admin/systemAdminRouter.js +6 -0
  164. package/server/routes/indexRouter.js +50 -6
  165. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  166. package/server/routes/payments/paymentsRouter.js +47 -0
  167. package/server/routes/static/staticPageRouter.js +30 -1
  168. package/server/utils/publicContact.js +31 -0
  169. package/utils/whatsapp/contactEnv.js +39 -0
  170. package/vite.config.mjs +5 -1
  171. package/app/modules/playModule/local/installYtDlp.js +0 -25
  172. package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
@@ -0,0 +1,564 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ ENV_FILE="${ENV_FILE:-$PROJECT_ROOT/.env}"
6
+
7
+ SESSION_ID_PATTERN='^[a-zA-Z0-9:_-]+$'
8
+ SESSION_ID_MAX_LENGTH=64
9
+
10
+ SESSION_ID=""
11
+ SESSION_PREFIX="work"
12
+ SESSION_WEIGHT=1
13
+ SET_PRIMARY=0
14
+ CONNECT_NOW=1
15
+ RESET_AUTH=0
16
+ CLEAR_AUTH_FILES=0
17
+ ALLOW_REUSE=0
18
+ PM2_SYNC=1
19
+ PM2_TARGET_APP=""
20
+ PM2_DRY_RUN=0
21
+ RUN_HEALTHCHECK=1
22
+ PM2_WAIT_TIMEOUT_SECONDS=45
23
+ PM2_WAIT_INTERVAL_SECONDS=3
24
+ HEALTHCHECK_WAIT_SECONDS=45
25
+ HEALTHCHECK_INTERVAL_SECONDS=3
26
+
27
+ log() {
28
+ printf '[new-whatsapp-session] %s\n' "$*"
29
+ }
30
+
31
+ fail() {
32
+ printf '[new-whatsapp-session] erro: %s\n' "$*" >&2
33
+ exit 1
34
+ }
35
+
36
+ require_cmd() {
37
+ if ! command -v "$1" >/dev/null 2>&1; then
38
+ fail "comando ausente: $1"
39
+ fi
40
+ }
41
+
42
+ usage() {
43
+ cat <<'EOF'
44
+ Uso:
45
+ bash scripts/new-whatsapp-session.sh [opcoes]
46
+
47
+ Opcoes:
48
+ --session <id> Define o session_id manualmente.
49
+ --prefix <valor> Prefixo para gerar session_id automatico (padrao: work).
50
+ --weight <1-1000> Peso da sessao em BAILEYS_SESSION_WEIGHTS (padrao: 1).
51
+ --primary Define a nova sessao como BAILEYS_PRIMARY_SESSION_ID.
52
+ --reuse Permite usar um session_id ja existente.
53
+ --reset-auth Limpa credenciais atuais da sessao no MySQL antes do QR.
54
+ --clear-auth-files Junto com --reset-auth, remove app/connection/auth/*.json.
55
+ --no-connect Apenas atualiza .env (nao abre conexao para QR agora).
56
+ --no-pm2-sync Nao sincroniza/reinicia app PM2 automaticamente.
57
+ --pm2-app <nome> Nome do app PM2 alvo (padrao: <PM2_APP_NAME>-production).
58
+ --pm2-dry-run Exibe a sincronizacao PM2 sem executar restart real.
59
+ --no-healthcheck Nao executa healthcheck HTTP apos sincronizar PM2.
60
+ --help Mostra esta ajuda.
61
+
62
+ Exemplos:
63
+ npm run new:work
64
+ npm run new:work -- --session suporte_2 --primary
65
+ bash scripts/new-whatsapp-session.sh --prefix operador --no-connect
66
+ bash scripts/new-whatsapp-session.sh --no-connect --pm2-app omnizap-system-production
67
+ EOF
68
+ }
69
+
70
+ strip_wrapping_quotes() {
71
+ local value="${1:-}"
72
+ if [[ "$value" == \"*\" && "$value" == *\" ]]; then
73
+ value="${value:1:${#value}-2}"
74
+ elif [[ "$value" == \'*\' && "$value" == *\' ]]; then
75
+ value="${value:1:${#value}-2}"
76
+ fi
77
+ printf '%s' "$value"
78
+ }
79
+
80
+ read_env_value() {
81
+ local key="$1"
82
+ local raw=""
83
+
84
+ if [[ -f "$ENV_FILE" ]]; then
85
+ raw="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 | cut -d'=' -f2- || true)"
86
+ fi
87
+
88
+ raw="${raw//$'\r'/}"
89
+ strip_wrapping_quotes "$raw"
90
+ }
91
+
92
+ split_entries() {
93
+ printf '%s' "${1:-}" | tr ',;\n' '\n' | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' | awk 'NF'
94
+ }
95
+
96
+ validate_session_id() {
97
+ local value="$1"
98
+ [[ -n "$value" ]] || return 1
99
+ [[ "${#value}" -le "$SESSION_ID_MAX_LENGTH" ]] || return 1
100
+ [[ "$value" =~ $SESSION_ID_PATTERN ]] || return 1
101
+ return 0
102
+ }
103
+
104
+ upsert_env_value() {
105
+ local key="$1"
106
+ local value="$2"
107
+ local temp_file
108
+ temp_file="$(mktemp)"
109
+
110
+ awk -v key="$key" -v value="$value" '
111
+ BEGIN {
112
+ replaced = 0;
113
+ prefix = key "=";
114
+ }
115
+ index($0, prefix) == 1 {
116
+ if (replaced == 0) {
117
+ print prefix value;
118
+ replaced = 1;
119
+ }
120
+ next;
121
+ }
122
+ { print; }
123
+ END {
124
+ if (replaced == 0) {
125
+ print prefix value;
126
+ }
127
+ }
128
+ ' "$ENV_FILE" >"$temp_file"
129
+
130
+ mv "$temp_file" "$ENV_FILE"
131
+ }
132
+
133
+ is_valid_weight() {
134
+ local value="$1"
135
+ [[ "$value" =~ ^[0-9]+$ ]] || return 1
136
+ ((value >= 1 && value <= 1000)) || return 1
137
+ return 0
138
+ }
139
+
140
+ normalize_bool() {
141
+ local value
142
+ value="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]' | xargs)"
143
+ if [[ -z "$value" ]]; then
144
+ printf 'unset'
145
+ return 0
146
+ fi
147
+ case "$value" in
148
+ 1|true|yes|y|on)
149
+ printf 'true'
150
+ ;;
151
+ 0|false|no|n|off)
152
+ printf 'false'
153
+ ;;
154
+ *)
155
+ printf 'unknown'
156
+ ;;
157
+ esac
158
+ }
159
+
160
+ pm2_get_status() {
161
+ local app_name="$1"
162
+ pm2 jlist 2>/dev/null | node -e "
163
+ const appName = String(process.argv[1] || '').trim();
164
+ let raw = '';
165
+ process.stdin.setEncoding('utf8');
166
+ process.stdin.on('data', (chunk) => {
167
+ raw += chunk;
168
+ });
169
+ process.stdin.on('end', () => {
170
+ try {
171
+ const list = JSON.parse(raw || '[]');
172
+ const target = Array.isArray(list) ? list.find((entry) => String(entry?.name || '') === appName) : null;
173
+ if (!target) {
174
+ process.stdout.write('missing');
175
+ return;
176
+ }
177
+ const status = String(target?.pm2_env?.status || '').trim().toLowerCase();
178
+ process.stdout.write(status || 'unknown');
179
+ } catch {
180
+ process.stdout.write('error');
181
+ }
182
+ });
183
+ " "$app_name"
184
+ }
185
+
186
+ pm2_detect_app_name() {
187
+ local preferred="$1"
188
+ pm2 jlist 2>/dev/null | node -e "
189
+ const preferred = String(process.argv[1] || '').trim();
190
+ let raw = '';
191
+ process.stdin.setEncoding('utf8');
192
+ process.stdin.on('data', (chunk) => {
193
+ raw += chunk;
194
+ });
195
+ process.stdin.on('end', () => {
196
+ try {
197
+ const list = JSON.parse(raw || '[]');
198
+ const names = (Array.isArray(list) ? list : [])
199
+ .map((entry) => String(entry?.name || '').trim())
200
+ .filter(Boolean);
201
+
202
+ if (preferred && names.includes(preferred)) {
203
+ process.stdout.write(preferred);
204
+ return;
205
+ }
206
+
207
+ if (names.includes('omnizap-production')) {
208
+ process.stdout.write('omnizap-production');
209
+ return;
210
+ }
211
+
212
+ const omnizapProduction = names.find((name) => /omnizap.*-production$/i.test(name));
213
+ if (omnizapProduction) {
214
+ process.stdout.write(omnizapProduction);
215
+ return;
216
+ }
217
+
218
+ const anyProduction = names.find((name) => /-production$/i.test(name));
219
+ if (anyProduction) {
220
+ process.stdout.write(anyProduction);
221
+ return;
222
+ }
223
+
224
+ process.stdout.write('');
225
+ } catch {
226
+ process.stdout.write('');
227
+ }
228
+ });
229
+ " "$preferred"
230
+ }
231
+
232
+ resolve_pm2_target_app() {
233
+ if [[ -n "$PM2_TARGET_APP" ]]; then
234
+ printf '%s' "$PM2_TARGET_APP"
235
+ return 0
236
+ fi
237
+
238
+ local pm2_base_name
239
+ pm2_base_name="${PM2_APP_NAME:-}"
240
+ if [[ -z "$pm2_base_name" ]]; then
241
+ pm2_base_name="$(read_env_value PM2_APP_NAME)"
242
+ fi
243
+ if [[ -n "$pm2_base_name" ]]; then
244
+ printf '%s' "${pm2_base_name}-production"
245
+ return 0
246
+ fi
247
+
248
+ printf '%s' "omnizap-production"
249
+ }
250
+
251
+ wait_pm2_online() {
252
+ local app_name="$1"
253
+ local waited=0
254
+
255
+ while (( waited <= PM2_WAIT_TIMEOUT_SECONDS )); do
256
+ local status
257
+ status="$(pm2_get_status "$app_name")"
258
+ if [[ "$status" == "online" ]]; then
259
+ log "PM2 app '$app_name' online."
260
+ return 0
261
+ fi
262
+ if [[ "$status" == "missing" ]]; then
263
+ fail "PM2 app '$app_name' nao encontrada durante sincronizacao."
264
+ fi
265
+ if [[ "$status" == "error" ]]; then
266
+ fail "Nao foi possivel consultar status do PM2 app '$app_name'."
267
+ fi
268
+
269
+ sleep "$PM2_WAIT_INTERVAL_SECONDS"
270
+ waited=$((waited + PM2_WAIT_INTERVAL_SECONDS))
271
+ done
272
+
273
+ fail "Timeout aguardando PM2 app '$app_name' ficar online."
274
+ }
275
+
276
+ run_http_healthcheck() {
277
+ local metrics_enabled_raw
278
+ metrics_enabled_raw="$(read_env_value METRICS_ENABLED)"
279
+ local metrics_enabled_state
280
+ metrics_enabled_state="$(normalize_bool "$metrics_enabled_raw")"
281
+ if [[ "$metrics_enabled_state" == "false" ]]; then
282
+ log "Healthcheck ignorado: METRICS_ENABLED=false."
283
+ return 0
284
+ fi
285
+
286
+ if ! command -v curl >/dev/null 2>&1; then
287
+ log "Healthcheck ignorado: comando 'curl' nao encontrado."
288
+ return 0
289
+ fi
290
+
291
+ local host port url
292
+ host="$(read_env_value METRICS_HOST)"
293
+ port="$(read_env_value METRICS_PORT)"
294
+ [[ -n "$host" ]] || host="127.0.0.1"
295
+ if [[ "$host" == "0.0.0.0" || "$host" == "::" ]]; then
296
+ host="127.0.0.1"
297
+ fi
298
+ if [[ ! "$port" =~ ^[0-9]+$ ]]; then
299
+ port="9102"
300
+ fi
301
+ url="http://${host}:${port}/healthz"
302
+
303
+ local waited=0
304
+ local last_code="000"
305
+ while (( waited <= HEALTHCHECK_WAIT_SECONDS )); do
306
+ local code
307
+ code="$(curl --silent --show-error --max-time 5 --output /dev/null --write-out '%{http_code}' "$url" || true)"
308
+ if [[ "$code" == "200" ]]; then
309
+ log "Healthcheck HTTP ok em $url."
310
+ return 0
311
+ fi
312
+ last_code="$code"
313
+ sleep "$HEALTHCHECK_INTERVAL_SECONDS"
314
+ waited=$((waited + HEALTHCHECK_INTERVAL_SECONDS))
315
+ done
316
+
317
+ fail "Healthcheck falhou em $url (ultimo HTTP: $last_code)."
318
+ }
319
+
320
+ sync_pm2_if_needed() {
321
+ if [[ "$PM2_SYNC" != "1" ]]; then
322
+ log "Sincronizacao PM2 desativada (--no-pm2-sync)."
323
+ return 0
324
+ fi
325
+
326
+ if ! command -v pm2 >/dev/null 2>&1; then
327
+ log "PM2 nao encontrado; sincronizacao automatica ignorada."
328
+ return 0
329
+ fi
330
+
331
+ local preferred_target
332
+ preferred_target="$(resolve_pm2_target_app)"
333
+ local app_target
334
+ app_target="$(pm2_detect_app_name "$preferred_target")"
335
+ if [[ -z "$app_target" ]]; then
336
+ log "Nenhum app PM2 de producao detectado para sincronizar."
337
+ return 0
338
+ fi
339
+
340
+ if [[ "$PM2_DRY_RUN" == "1" ]]; then
341
+ log "PM2 dry-run: executaria 'pm2 restart $app_target --update-env'."
342
+ return 0
343
+ fi
344
+
345
+ log "Sincronizando PM2 app '$app_target'..."
346
+ pm2 restart "$app_target" --update-env >/dev/null
347
+ wait_pm2_online "$app_target"
348
+
349
+ if [[ "$RUN_HEALTHCHECK" == "1" ]]; then
350
+ run_http_healthcheck
351
+ else
352
+ log "Healthcheck desativado (--no-healthcheck)."
353
+ fi
354
+ }
355
+
356
+ while [[ $# -gt 0 ]]; do
357
+ case "$1" in
358
+ --session)
359
+ [[ $# -ge 2 ]] || fail "faltou valor para --session"
360
+ SESSION_ID="$2"
361
+ shift 2
362
+ ;;
363
+ --prefix)
364
+ [[ $# -ge 2 ]] || fail "faltou valor para --prefix"
365
+ SESSION_PREFIX="$2"
366
+ shift 2
367
+ ;;
368
+ --weight)
369
+ [[ $# -ge 2 ]] || fail "faltou valor para --weight"
370
+ SESSION_WEIGHT="$2"
371
+ shift 2
372
+ ;;
373
+ --primary)
374
+ SET_PRIMARY=1
375
+ shift
376
+ ;;
377
+ --reuse)
378
+ ALLOW_REUSE=1
379
+ shift
380
+ ;;
381
+ --reset-auth)
382
+ RESET_AUTH=1
383
+ shift
384
+ ;;
385
+ --clear-auth-files)
386
+ CLEAR_AUTH_FILES=1
387
+ shift
388
+ ;;
389
+ --no-connect)
390
+ CONNECT_NOW=0
391
+ shift
392
+ ;;
393
+ --no-pm2-sync)
394
+ PM2_SYNC=0
395
+ shift
396
+ ;;
397
+ --pm2-app)
398
+ [[ $# -ge 2 ]] || fail "faltou valor para --pm2-app"
399
+ PM2_TARGET_APP="$2"
400
+ shift 2
401
+ ;;
402
+ --pm2-dry-run)
403
+ PM2_DRY_RUN=1
404
+ shift
405
+ ;;
406
+ --no-healthcheck)
407
+ RUN_HEALTHCHECK=0
408
+ shift
409
+ ;;
410
+ -h|--help)
411
+ usage
412
+ exit 0
413
+ ;;
414
+ *)
415
+ fail "opcao invalida: $1 (use --help)"
416
+ ;;
417
+ esac
418
+ done
419
+
420
+ [[ -f "$ENV_FILE" ]] || fail "arquivo .env nao encontrado em: $ENV_FILE"
421
+ is_valid_weight "$SESSION_WEIGHT" || fail "peso invalido em --weight: $SESSION_WEIGHT (use 1..1000)"
422
+
423
+ if [[ -n "$SESSION_ID" ]]; then
424
+ validate_session_id "$SESSION_ID" || fail "session_id invalido: \"$SESSION_ID\""
425
+ else
426
+ validate_session_id "$SESSION_PREFIX" || fail "prefixo invalido para session_id: \"$SESSION_PREFIX\""
427
+ fi
428
+
429
+ declare -A SEEN_SESSION_IDS=()
430
+ declare -a SESSION_IDS=()
431
+
432
+ existing_session_ids_raw="$(read_env_value BAILEYS_SESSION_IDS)"
433
+ legacy_session_id_raw="$(read_env_value BAILEYS_AUTH_SESSION_ID)"
434
+ if [[ -z "$existing_session_ids_raw" ]]; then
435
+ existing_session_ids_raw="$legacy_session_id_raw"
436
+ fi
437
+ if [[ -z "$existing_session_ids_raw" ]]; then
438
+ existing_session_ids_raw="default"
439
+ fi
440
+
441
+ while IFS= read -r candidate; do
442
+ [[ -n "$candidate" ]] || continue
443
+ if ! validate_session_id "$candidate"; then
444
+ log "ignorado session_id invalido no .env: $candidate"
445
+ continue
446
+ fi
447
+ if [[ -z "${SEEN_SESSION_IDS[$candidate]+x}" ]]; then
448
+ SEEN_SESSION_IDS["$candidate"]=1
449
+ SESSION_IDS+=("$candidate")
450
+ fi
451
+ done < <(split_entries "$existing_session_ids_raw")
452
+
453
+ if [[ "${#SESSION_IDS[@]}" -eq 0 ]]; then
454
+ SESSION_IDS=("default")
455
+ SEEN_SESSION_IDS["default"]=1
456
+ fi
457
+
458
+ if [[ -z "$SESSION_ID" ]]; then
459
+ base_id="${SESSION_PREFIX}-$(date -u +%Y%m%d%H%M%S)"
460
+ generated_id="$base_id"
461
+ suffix=1
462
+ while [[ -n "${SEEN_SESSION_IDS[$generated_id]+x}" ]]; do
463
+ suffix=$((suffix + 1))
464
+ generated_id="${base_id}-${suffix}"
465
+ done
466
+ SESSION_ID="$generated_id"
467
+ fi
468
+
469
+ if [[ -n "${SEEN_SESSION_IDS[$SESSION_ID]+x}" && "$ALLOW_REUSE" != "1" ]]; then
470
+ fail "session_id \"$SESSION_ID\" ja existe. Use --reuse para permitir reutilizacao."
471
+ fi
472
+
473
+ if [[ -z "${SEEN_SESSION_IDS[$SESSION_ID]+x}" ]]; then
474
+ SESSION_IDS+=("$SESSION_ID")
475
+ SEEN_SESSION_IDS["$SESSION_ID"]=1
476
+ fi
477
+
478
+ current_primary_session_id="$(read_env_value BAILEYS_PRIMARY_SESSION_ID)"
479
+ if ! validate_session_id "$current_primary_session_id"; then
480
+ current_primary_session_id="${SESSION_IDS[0]}"
481
+ fi
482
+ if [[ -z "${SEEN_SESSION_IDS[$current_primary_session_id]+x}" ]]; then
483
+ current_primary_session_id="${SESSION_IDS[0]}"
484
+ fi
485
+
486
+ if [[ "$SET_PRIMARY" == "1" ]]; then
487
+ current_primary_session_id="$SESSION_ID"
488
+ fi
489
+
490
+ declare -A SESSION_WEIGHTS=()
491
+ weights_raw="$(read_env_value BAILEYS_SESSION_WEIGHTS)"
492
+ while IFS= read -r raw_entry; do
493
+ [[ -n "$raw_entry" ]] || continue
494
+ separator='='
495
+ if [[ "$raw_entry" == *":"* && "$raw_entry" != *"="* ]]; then
496
+ separator=':'
497
+ fi
498
+ if [[ "$raw_entry" != *"$separator"* ]]; then
499
+ continue
500
+ fi
501
+
502
+ raw_session="${raw_entry%%"$separator"*}"
503
+ raw_weight="${raw_entry#*"$separator"}"
504
+ if ! validate_session_id "$raw_session"; then
505
+ continue
506
+ fi
507
+ if is_valid_weight "$raw_weight"; then
508
+ SESSION_WEIGHTS["$raw_session"]="$raw_weight"
509
+ fi
510
+ done < <(split_entries "$weights_raw")
511
+
512
+ SESSION_WEIGHTS["$SESSION_ID"]="$SESSION_WEIGHT"
513
+ for existing_session in "${SESSION_IDS[@]}"; do
514
+ if [[ -z "${SESSION_WEIGHTS[$existing_session]+x}" ]]; then
515
+ SESSION_WEIGHTS["$existing_session"]=1
516
+ fi
517
+ done
518
+
519
+ session_ids_value="$(IFS=,; printf '%s' "${SESSION_IDS[*]}")"
520
+ declare -a weight_entries=()
521
+ for listed_session in "${SESSION_IDS[@]}"; do
522
+ weight_entries+=("${listed_session}=${SESSION_WEIGHTS[$listed_session]}")
523
+ done
524
+ weights_value="$(IFS=,; printf '%s' "${weight_entries[*]}")"
525
+
526
+ upsert_env_value "BAILEYS_SESSION_IDS" "$session_ids_value"
527
+ upsert_env_value "BAILEYS_PRIMARY_SESSION_ID" "$current_primary_session_id"
528
+ upsert_env_value "BAILEYS_SESSION_WEIGHTS" "$weights_value"
529
+
530
+ legacy_auth_session_id="$(read_env_value BAILEYS_AUTH_SESSION_ID)"
531
+ if [[ -z "$legacy_auth_session_id" || "$SET_PRIMARY" == "1" ]]; then
532
+ upsert_env_value "BAILEYS_AUTH_SESSION_ID" "$current_primary_session_id"
533
+ fi
534
+
535
+ if [[ "$RESET_AUTH" == "1" ]]; then
536
+ reset_args=(--session "$SESSION_ID")
537
+ if [[ "$CLEAR_AUTH_FILES" == "1" ]]; then
538
+ reset_args+=(--clear-auth-files)
539
+ fi
540
+ bash "$PROJECT_ROOT/scripts/clear-whatsapp-session.sh" "${reset_args[@]}"
541
+ fi
542
+
543
+ log "Sessao preparada com sucesso."
544
+ log "session_id: $SESSION_ID"
545
+ log "BAILEYS_SESSION_IDS: $session_ids_value"
546
+ log "BAILEYS_PRIMARY_SESSION_ID: $current_primary_session_id"
547
+ sync_pm2_if_needed
548
+
549
+ if [[ "$CONNECT_NOW" == "1" ]]; then
550
+ require_cmd node
551
+ log "Inicializando schema do banco..."
552
+ node "$PROJECT_ROOT/database/init.js"
553
+
554
+ log "Abrindo conexao da sessao '$SESSION_ID' para leitura do QR Code..."
555
+ export BAILEYS_SESSION_IDS="$SESSION_ID"
556
+ export BAILEYS_PRIMARY_SESSION_ID="$SESSION_ID"
557
+ export BAILEYS_AUTH_SESSION_ID="$SESSION_ID"
558
+
559
+ log "Apos conectar no WhatsApp, use Ctrl+C para encerrar."
560
+ exec node "$PROJECT_ROOT/app/connection/socketController.js"
561
+ fi
562
+
563
+ log "Concluido sem abrir conexao. Para abrir QR agora use:"
564
+ log " BAILEYS_SESSION_IDS=$SESSION_ID BAILEYS_PRIMARY_SESSION_ID=$SESSION_ID BAILEYS_AUTH_SESSION_ID=$SESSION_ID node app/connection/socketController.js"