@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,992 @@
1
+ import makeWASocket, { useMultiFileAuthState, DisconnectReason, Browsers, getAggregateVotesInPollMessage } from '@whiskeysockets/baileys';
2
+
3
+ import NodeCache from 'node-cache';
4
+ import { resolveBaileysVersion } from '../config/baileysConfig.js';
5
+
6
+ import { Boom } from '@hapi/boom';
7
+ import qrcode from 'qrcode-terminal';
8
+ import path from 'node:path';
9
+
10
+ import pino from 'pino';
11
+ import logger from '../utils/logger/loggerModule.js';
12
+ import { handleMessages } from '../controllers/messageController.js';
13
+ import { syncNewsBroadcastService } from '../services/newsBroadcastService.js';
14
+ import { setActiveSocket as storeActiveSocket } from '../services/socketState.js';
15
+ import { recordError, recordMessagesUpsert } from '../observability/metrics.js';
16
+ import { resolveCaptchaByReaction } from '../services/captchaService.js';
17
+
18
+ import { handleGroupUpdate as handleGroupParticipantsEvent, handleGroupJoinRequest } from '../modules/adminModule/groupEventHandlers.js';
19
+
20
+ import { findBy, findById, remove } from '../../database/index.js';
21
+ import { extractSenderInfoFromMessage, primeLidCache, resolveUserIdCached, isLidUserId, isWhatsAppUserId } from '../services/lidMapService.js';
22
+ import { queueChatUpdate, queueLidUpdate, queueMessageInsert } from '../services/dbWriteQueue.js';
23
+ import { buildGroupMetadataFromGroup, buildGroupMetadataFromUpdate, upsertGroupMetadata } from '../services/groupMetadataService.js';
24
+ import { buildMessageData } from '../services/messagePersistenceService.js';
25
+
26
+ import { fileURLToPath } from 'node:url';
27
+
28
+ const __filename = fileURLToPath(import.meta.url);
29
+ const __dirname = path.dirname(__filename);
30
+
31
+ const parseEnvBool = (value, fallback) => {
32
+ if (value === undefined || value === null || value === '') return fallback;
33
+ const normalized = String(value).trim().toLowerCase();
34
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
35
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
36
+ return fallback;
37
+ };
38
+
39
+ const parseEnvInt = (value, fallback, min, max) => {
40
+ const parsed = Number(value);
41
+ if (!Number.isFinite(parsed)) return fallback;
42
+ return Math.max(min, Math.min(max, Math.floor(parsed)));
43
+ };
44
+
45
+ const IS_PRODUCTION = String(process.env.NODE_ENV || '').trim().toLowerCase() === 'production';
46
+ const MSG_RETRY_CACHE_TTL_SECONDS = parseEnvInt(process.env.BAILEYS_MSG_RETRY_CACHE_TTL_SECONDS, 600, 60, 24 * 60 * 60);
47
+ const MSG_RETRY_CACHE_CHECKPERIOD_SECONDS = parseEnvInt(process.env.BAILEYS_MSG_RETRY_CACHE_CHECKPERIOD_SECONDS, 120, 30, 3600);
48
+ const BAILEYS_EVENT_LOG_ENABLED = parseEnvBool(process.env.BAILEYS_EVENT_LOG_ENABLED, !IS_PRODUCTION);
49
+ const BAILEYS_RECONNECT_ATTEMPT_RESET_MS = parseEnvInt(
50
+ process.env.BAILEYS_RECONNECT_ATTEMPT_RESET_MS,
51
+ 10 * 60 * 1000,
52
+ 60 * 1000,
53
+ 24 * 60 * 60 * 1000,
54
+ );
55
+ const GROUP_SYNC_ON_CONNECT = parseEnvBool(process.env.GROUP_SYNC_ON_CONNECT, true);
56
+ const GROUP_SYNC_TIMEOUT_MS = parseEnvInt(process.env.GROUP_SYNC_TIMEOUT_MS, 30 * 1000, 5 * 1000, 120 * 1000);
57
+ const GROUP_SYNC_MAX_GROUPS = parseEnvInt(process.env.GROUP_SYNC_MAX_GROUPS, 0, 0, 10_000);
58
+ const GROUP_SYNC_BATCH_SIZE = parseEnvInt(process.env.GROUP_SYNC_BATCH_SIZE, 50, 1, 1000);
59
+
60
+ let activeSocket = null;
61
+ let connectionAttempts = 0;
62
+ let reconnectWindowStartedAt = 0;
63
+ const msgRetryCounterCache = new NodeCache({
64
+ stdTTL: MSG_RETRY_CACHE_TTL_SECONDS,
65
+ checkperiod: MSG_RETRY_CACHE_CHECKPERIOD_SECONDS,
66
+ useClones: false,
67
+ });
68
+ const MAX_CONNECTION_ATTEMPTS = 5;
69
+ const INITIAL_RECONNECT_DELAY = 3000;
70
+ let reconnectTimeout = null;
71
+ let connectPromise = null;
72
+ let socketGeneration = 0;
73
+ const BAILEYS_EVENT_NAMES = ['connection.update', 'creds.update', 'messaging-history.set', 'chats.upsert', 'chats.update', 'lid-mapping.update', 'chats.delete', 'presence.update', 'contacts.upsert', 'contacts.update', 'messages.delete', 'messages.update', 'messages.media-update', 'messages.upsert', 'messages.reaction', 'message-receipt.update', 'groups.upsert', 'groups.update', 'group-participants.update', 'group.join-request', 'group.member-tag.update', 'blocklist.set', 'blocklist.update', 'call', 'labels.edit', 'labels.association', 'newsletter.reaction', 'newsletter.view', 'newsletter-participants.update', 'newsletter-settings.update', 'chats.lock', 'settings.update'];
74
+ const BAILEYS_EVENTS_WITH_INTERNAL_LOG = new Set(['creds.update', 'connection.update', 'messages.upsert', 'messages.update', 'groups.update', 'group-participants.update']);
75
+ const BAILEYS_NOISY_EVENTS_IN_PRODUCTION = new Set(['presence.update']);
76
+
77
+ const summarizeBaileysEventPayload = (eventName, payload) => {
78
+ if (payload === null) return { payloadType: 'null' };
79
+ if (payload === undefined) return { payloadType: 'undefined' };
80
+ if (Buffer.isBuffer(payload)) {
81
+ return { payloadType: 'buffer', bytes: payload.length };
82
+ }
83
+
84
+ if (Array.isArray(payload)) {
85
+ const summary = { payloadType: 'array', items: payload.length };
86
+ if (payload.length > 0 && payload[0] && typeof payload[0] === 'object') {
87
+ summary.sampleKeys = Object.keys(payload[0]).slice(0, 6);
88
+ }
89
+ return summary;
90
+ }
91
+
92
+ if (typeof payload !== 'object') {
93
+ if (typeof payload === 'string') {
94
+ return { payloadType: 'string', length: payload.length };
95
+ }
96
+ return { payloadType: typeof payload, value: payload };
97
+ }
98
+
99
+ const summary = { payloadType: 'object', keys: Object.keys(payload).slice(0, 10) };
100
+
101
+ switch (eventName) {
102
+ case 'messaging-history.set':
103
+ summary.chats = Array.isArray(payload.chats) ? payload.chats.length : 0;
104
+ summary.contacts = Array.isArray(payload.contacts) ? payload.contacts.length : 0;
105
+ summary.messages = Array.isArray(payload.messages) ? payload.messages.length : 0;
106
+ summary.isLatest = payload.isLatest ?? null;
107
+ summary.progress = payload.progress ?? null;
108
+ summary.syncType = payload.syncType ?? null;
109
+ break;
110
+ case 'presence.update':
111
+ summary.id = payload.id ?? null;
112
+ summary.presencesCount = payload.presences ? Object.keys(payload.presences).length : 0;
113
+ break;
114
+ case 'chats.lock':
115
+ summary.id = payload.id ?? null;
116
+ summary.locked = payload.locked ?? null;
117
+ break;
118
+ case 'settings.update':
119
+ summary.setting = payload.setting ?? null;
120
+ break;
121
+ case 'messages.delete':
122
+ if (payload.all === true) {
123
+ summary.all = true;
124
+ summary.jid = payload.jid ?? null;
125
+ } else if (Array.isArray(payload.keys)) {
126
+ summary.keysCount = payload.keys.length;
127
+ }
128
+ break;
129
+ case 'blocklist.set':
130
+ summary.blocklistCount = Array.isArray(payload.blocklist) ? payload.blocklist.length : 0;
131
+ break;
132
+ case 'blocklist.update':
133
+ summary.blocklistCount = Array.isArray(payload.blocklist) ? payload.blocklist.length : 0;
134
+ summary.type = payload.type ?? null;
135
+ break;
136
+ case 'group.join-request':
137
+ summary.groupId = payload.id ?? null;
138
+ summary.participant = payload.participant ?? null;
139
+ summary.action = payload.action ?? null;
140
+ summary.method = payload.method ?? null;
141
+ break;
142
+ case 'group.member-tag.update':
143
+ summary.groupId = payload.groupId ?? null;
144
+ summary.participant = payload.participant ?? null;
145
+ summary.label = payload.label ?? null;
146
+ break;
147
+ case 'labels.association':
148
+ summary.type = payload.type ?? null;
149
+ summary.labelId = payload.association?.labelId ?? null;
150
+ summary.chatId = payload.association?.chatId ?? null;
151
+ break;
152
+ case 'labels.edit':
153
+ summary.labelId = payload.id ?? null;
154
+ summary.labelName = payload.name ?? payload.label ?? null;
155
+ break;
156
+ case 'newsletter.reaction':
157
+ summary.id = payload.id ?? null;
158
+ summary.serverId = payload.server_id ?? null;
159
+ summary.reactionCode = payload.reaction?.code ?? null;
160
+ summary.reactionCount = payload.reaction?.count ?? null;
161
+ summary.reactionRemoved = payload.reaction?.removed ?? null;
162
+ break;
163
+ case 'newsletter.view':
164
+ summary.id = payload.id ?? null;
165
+ summary.serverId = payload.server_id ?? null;
166
+ summary.count = payload.count ?? null;
167
+ break;
168
+ case 'newsletter-participants.update':
169
+ summary.id = payload.id ?? null;
170
+ summary.user = payload.user ?? null;
171
+ summary.action = payload.action ?? null;
172
+ summary.newRole = payload.new_role ?? null;
173
+ break;
174
+ case 'newsletter-settings.update':
175
+ summary.id = payload.id ?? null;
176
+ summary.updateKeys = payload.update ? Object.keys(payload.update).slice(0, 6) : [];
177
+ break;
178
+ case 'lid-mapping.update':
179
+ summary.lid = payload.lid ?? null;
180
+ summary.pn = payload.pn ?? payload.jid ?? null;
181
+ break;
182
+ default:
183
+ break;
184
+ }
185
+
186
+ return summary;
187
+ };
188
+
189
+ const shouldLogBaileysEvent = (eventName) => {
190
+ if (!BAILEYS_EVENT_LOG_ENABLED) return false;
191
+ if (IS_PRODUCTION && BAILEYS_NOISY_EVENTS_IN_PRODUCTION.has(eventName)) return false;
192
+ return true;
193
+ };
194
+
195
+ const registerBaileysEventLoggers = (sock) => {
196
+ const eventsToLog = BAILEYS_EVENT_NAMES.filter(
197
+ (eventName) => !BAILEYS_EVENTS_WITH_INTERNAL_LOG.has(eventName) && shouldLogBaileysEvent(eventName),
198
+ );
199
+
200
+ for (const eventName of eventsToLog) {
201
+ sock.ev.on(eventName, (payload) => {
202
+ const summary = summarizeBaileysEventPayload(eventName, payload);
203
+ logger.debug('Evento Baileys recebido.', {
204
+ action: 'baileys_event',
205
+ event: eventName,
206
+ ...summary,
207
+ timestamp: new Date().toISOString(),
208
+ });
209
+ });
210
+ }
211
+
212
+ logger.debug('Loggers de eventos Baileys registrados.', {
213
+ action: 'baileys_event_logger_ready',
214
+ enabled: BAILEYS_EVENT_LOG_ENABLED,
215
+ eventsCount: eventsToLog.length,
216
+ events: eventsToLog,
217
+ });
218
+ };
219
+
220
+ /**
221
+ * Faz parse seguro de JSON com suporte a Buffer e retorna fallback em caso de erro.
222
+ * @param {unknown} value - Valor a ser interpretado.
223
+ * @param {any} fallback - Valor retornado quando o parse falha ou o valor é inválido.
224
+ * @returns {any} Objeto parseado ou fallback.
225
+ */
226
+ const safeJsonParse = (value, fallback) => {
227
+ if (value === null || value === undefined) return fallback;
228
+ if (Buffer.isBuffer(value)) {
229
+ return safeJsonParse(value.toString('utf8'), fallback);
230
+ }
231
+ if (typeof value === 'object') return value;
232
+ if (typeof value !== 'string') return fallback;
233
+ try {
234
+ return JSON.parse(value);
235
+ } catch (error) {
236
+ logger.warn('Falha ao fazer parse de JSON armazenado.', {
237
+ error: error.message,
238
+ });
239
+ return fallback;
240
+ }
241
+ };
242
+
243
+ /**
244
+ * Normaliza PN para JID de WhatsApp quando o payload vier sem domínio.
245
+ * @param {string|null|undefined} pn
246
+ * @returns {string|null}
247
+ */
248
+ const normalizePnToJid = (pn) => {
249
+ if (!pn || typeof pn !== 'string') return null;
250
+ const normalized = pn.trim();
251
+ if (!normalized) return null;
252
+ if (isWhatsAppUserId(normalized)) return normalized;
253
+ if (/^\d+(?::\d+)?$/.test(normalized)) return `${normalized}@s.whatsapp.net`;
254
+ return null;
255
+ };
256
+
257
+ /**
258
+ * Persiste mensagens recebidas quando o tipo do upsert permite salvamento.
259
+ * @async
260
+ * @param {Array<import('@whiskeysockets/baileys').WAMessage>} incomingMessages - Mensagens recebidas.
261
+ * @param {'append' | 'notify' | string} type - Tipo do evento de upsert.
262
+ * @returns {Promise<void>} Conclusão da persistência.
263
+ */
264
+ async function persistIncomingMessages(incomingMessages, type) {
265
+ if (type !== 'append' && type !== 'notify') return;
266
+
267
+ const entries = [];
268
+ const lidsToPrime = new Set();
269
+
270
+ for (const msg of incomingMessages) {
271
+ if (!msg.message || msg.key.remoteJid === 'status@broadcast') continue;
272
+ const senderInfo = extractSenderInfoFromMessage(msg);
273
+ if (senderInfo.lid) lidsToPrime.add(senderInfo.lid);
274
+ entries.push({ msg, senderInfo });
275
+ }
276
+
277
+ if (lidsToPrime.size > 0) {
278
+ try {
279
+ await primeLidCache(Array.from(lidsToPrime));
280
+ } catch (error) {
281
+ logger.warn('Falha ao aquecer cache de LID.', { error: error.message });
282
+ }
283
+ }
284
+
285
+ for (const { msg, senderInfo } of entries) {
286
+ if (senderInfo.lid) {
287
+ queueLidUpdate(senderInfo.lid, senderInfo.jid, 'message');
288
+ }
289
+
290
+ const canonicalSenderId = resolveUserIdCached(senderInfo) || msg.key.participant || msg.key.remoteJid;
291
+
292
+ const messageData = buildMessageData(msg, canonicalSenderId);
293
+ queueMessageInsert(messageData);
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Recupera mensagem armazenada para suporte a recursos (ex.: enquetes) do Baileys.
299
+ * @async
300
+ * @param {import('@whiskeysockets/baileys').WAMessageKey} key - Chave da mensagem.
301
+ * @returns {Promise<import('@whiskeysockets/baileys').proto.IMessage | undefined>} Conteúdo da mensagem armazenada.
302
+ */
303
+ async function getStoredMessage(key) {
304
+ const messageId = key?.id;
305
+ const remoteJid = key?.remoteJid;
306
+ if (!messageId || !remoteJid) return undefined;
307
+
308
+ try {
309
+ const results = await findBy('messages', { message_id: messageId, chat_id: remoteJid }, { limit: 1 });
310
+ const record = results?.[0];
311
+ const stored = safeJsonParse(record?.raw_message, null);
312
+ if (record?.raw_message && !stored) {
313
+ logger.error('Falha ao interpretar raw_message armazenado.', {
314
+ messageId,
315
+ remoteJid,
316
+ });
317
+ }
318
+ return stored?.message ?? undefined;
319
+ } catch (error) {
320
+ logger.error('Erro ao buscar mensagem armazenada no banco:', {
321
+ error: error.message,
322
+ messageId,
323
+ remoteJid,
324
+ });
325
+ return undefined;
326
+ }
327
+ }
328
+
329
+ const clearReconnectTimeout = () => {
330
+ if (!reconnectTimeout) return;
331
+ clearTimeout(reconnectTimeout);
332
+ reconnectTimeout = null;
333
+ };
334
+
335
+ const resetReconnectState = () => {
336
+ connectionAttempts = 0;
337
+ reconnectWindowStartedAt = 0;
338
+ };
339
+
340
+ const getNextReconnectAttempt = () => {
341
+ const now = Date.now();
342
+ if (!reconnectWindowStartedAt || now - reconnectWindowStartedAt >= BAILEYS_RECONNECT_ATTEMPT_RESET_MS) {
343
+ reconnectWindowStartedAt = now;
344
+ connectionAttempts = 0;
345
+ }
346
+ connectionAttempts += 1;
347
+ return connectionAttempts;
348
+ };
349
+
350
+ const scheduleReconnect = (delay) => {
351
+ if (reconnectTimeout) return;
352
+ reconnectTimeout = setTimeout(
353
+ () => {
354
+ reconnectTimeout = null;
355
+ connectToWhatsApp().catch((error) => {
356
+ logger.error('Falha ao executar reconexão agendada.', {
357
+ action: 'reconnect_schedule_failure',
358
+ errorMessage: error?.message,
359
+ stack: error?.stack,
360
+ timestamp: new Date().toISOString(),
361
+ });
362
+ });
363
+ },
364
+ Math.max(0, Number(delay) || 0),
365
+ );
366
+ };
367
+
368
+ const isSocketOpen = (socket) => {
369
+ if (!socket?.ws) return false;
370
+ if (typeof socket.ws.isOpen === 'boolean') return socket.ws.isOpen;
371
+ return socket.ws.readyState === 1;
372
+ };
373
+
374
+ const withTimeout = (promise, timeoutMs, timeoutLabel = 'operation_timeout') =>
375
+ Promise.race([
376
+ promise,
377
+ new Promise((_, reject) => {
378
+ setTimeout(() => reject(new Error(timeoutLabel)), timeoutMs);
379
+ }),
380
+ ]);
381
+
382
+ const syncGroupsOnConnectionOpen = async (sock) => {
383
+ if (!GROUP_SYNC_ON_CONNECT) {
384
+ logger.info('Sincronização de grupos no connect desativada por configuração.', {
385
+ action: 'groups_sync_disabled',
386
+ timestamp: new Date().toISOString(),
387
+ });
388
+ return;
389
+ }
390
+
391
+ const allGroups = await withTimeout(
392
+ sock.groupFetchAllParticipating(),
393
+ GROUP_SYNC_TIMEOUT_MS,
394
+ `groups_sync_timeout_${GROUP_SYNC_TIMEOUT_MS}ms`,
395
+ );
396
+ const allGroupEntries = Object.values(allGroups || {});
397
+ const selectedGroups =
398
+ GROUP_SYNC_MAX_GROUPS > 0
399
+ ? allGroupEntries.slice(0, GROUP_SYNC_MAX_GROUPS)
400
+ : allGroupEntries;
401
+
402
+ let syncedCount = 0;
403
+ let failedCount = 0;
404
+
405
+ for (let offset = 0; offset < selectedGroups.length; offset += GROUP_SYNC_BATCH_SIZE) {
406
+ const batch = selectedGroups.slice(offset, offset + GROUP_SYNC_BATCH_SIZE);
407
+ const results = await Promise.allSettled(
408
+ batch.map((group) =>
409
+ upsertGroupMetadata(group.id, buildGroupMetadataFromGroup(group), {
410
+ mergeExisting: false,
411
+ })),
412
+ );
413
+ for (const result of results) {
414
+ if (result.status === 'fulfilled') syncedCount += 1;
415
+ else failedCount += 1;
416
+ }
417
+ }
418
+
419
+ logger.info('📁 Metadados de grupos sincronizados com MySQL.', {
420
+ action: 'groups_synced',
421
+ totalFetched: allGroupEntries.length,
422
+ totalSynced: syncedCount,
423
+ totalFailed: failedCount,
424
+ totalSkipped: Math.max(0, allGroupEntries.length - selectedGroups.length),
425
+ batchSize: GROUP_SYNC_BATCH_SIZE,
426
+ maxGroups: GROUP_SYNC_MAX_GROUPS > 0 ? GROUP_SYNC_MAX_GROUPS : null,
427
+ timeoutMs: GROUP_SYNC_TIMEOUT_MS,
428
+ timestamp: new Date().toISOString(),
429
+ });
430
+ };
431
+
432
+ /**
433
+ * Inicia e gerencia a conexão com o WhatsApp usando o Baileys.
434
+ * Configura autenticação, cria o socket e registra handlers de eventos.
435
+ * @async
436
+ * @returns {Promise<void>} Conclusão da inicialização e do registro de handlers.
437
+ * @throws {Error} Lança erro se a conexão inicial falhar.
438
+ */
439
+ export async function connectToWhatsApp() {
440
+ if (connectPromise) {
441
+ return connectPromise;
442
+ }
443
+
444
+ if (isSocketOpen(activeSocket)) {
445
+ return;
446
+ }
447
+
448
+ logger.info('Iniciando conexão com o WhatsApp...', {
449
+ action: 'connect_init',
450
+ timestamp: new Date().toISOString(),
451
+ });
452
+ connectPromise = (async () => {
453
+ clearReconnectTimeout();
454
+ const generation = ++socketGeneration;
455
+ const authPath = path.join(__dirname, 'auth');
456
+ const { state, saveCreds } = await useMultiFileAuthState(authPath);
457
+
458
+ const version = await resolveBaileysVersion();
459
+
460
+ logger.debug('Dados de autenticação carregados com sucesso.', {
461
+ authPath,
462
+ version,
463
+ generation,
464
+ });
465
+
466
+ const sock = makeWASocket({
467
+ version,
468
+ auth: state,
469
+ logger: pino({ level: 'silent' }),
470
+ browser: Browsers.macOS('Desktop'),
471
+ qrTimeout: 30000,
472
+ syncFullHistory: false,
473
+ markOnlineOnConnect: true,
474
+ msgRetryCounterCache,
475
+ maxMsgRetryCount: 5,
476
+ retryRequestDelayMs: 250,
477
+ getMessage: getStoredMessage,
478
+ });
479
+
480
+ activeSocket = sock;
481
+ storeActiveSocket(sock);
482
+
483
+ const isCurrentSocket = () => activeSocket === sock && generation === socketGeneration;
484
+
485
+ sock.ev.on('creds.update', async () => {
486
+ if (!isCurrentSocket()) return;
487
+ logger.debug('Atualizando credenciais de autenticação...', {
488
+ action: 'creds_update',
489
+ timestamp: new Date().toISOString(),
490
+ });
491
+ await saveCreds();
492
+ });
493
+
494
+ sock.ev.on('connection.update', (update) => {
495
+ if (!isCurrentSocket()) return;
496
+ handleConnectionUpdate(update, sock);
497
+ if (update.connection === 'open') {
498
+ syncNewsBroadcastService();
499
+ }
500
+ logger.debug('Estado da conexão atualizado.', {
501
+ action: 'connection_update',
502
+ status: update.connection,
503
+ lastDisconnect: update.lastDisconnect?.error?.message || null,
504
+ isNewLogin: update.isNewLogin || false,
505
+ timestamp: new Date().toISOString(),
506
+ });
507
+ });
508
+
509
+ sock.ev.on('messages.upsert', (update) => {
510
+ if (!isCurrentSocket()) return;
511
+ const start = process.hrtime.bigint();
512
+ const messagesCount = Array.isArray(update?.messages) ? update.messages.length : 0;
513
+ const eventType = update?.type || 'unknown';
514
+ try {
515
+ logger.debug('Novo(s) evento(s) em messages.upsert', {
516
+ action: 'messages_upsert',
517
+ type: update.type,
518
+ messagesCount: update.messages.length,
519
+ remoteJid: update.messages[0]?.key.remoteJid || null,
520
+ });
521
+ const persistPromise = persistIncomingMessages(update.messages, update.type).catch((error) => {
522
+ logger.error('Erro ao persistir mensagens no banco de dados:', {
523
+ error: error.message,
524
+ });
525
+ recordError('messages_upsert');
526
+ });
527
+ const handlePromise = handleMessages(update, sock).catch((error) => {
528
+ recordError('messages_upsert');
529
+ throw error;
530
+ });
531
+
532
+ Promise.allSettled([persistPromise, handlePromise]).then((results) => {
533
+ const ok = results.every((result) => result.status === 'fulfilled');
534
+ const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
535
+ recordMessagesUpsert({
536
+ durationMs,
537
+ type: eventType,
538
+ messagesCount,
539
+ ok,
540
+ });
541
+ });
542
+ } catch (error) {
543
+ logger.error('Erro no evento messages.upsert:', {
544
+ error: error.message,
545
+ stack: error.stack,
546
+ action: 'messages_upsert_error',
547
+ });
548
+ recordError('messages_upsert');
549
+ const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
550
+ recordMessagesUpsert({
551
+ durationMs,
552
+ type: eventType,
553
+ messagesCount,
554
+ ok: false,
555
+ });
556
+ }
557
+ });
558
+
559
+ sock.ev.on('chats.upsert', (newChats) => {
560
+ if (!isCurrentSocket()) return;
561
+ for (const chat of newChats) {
562
+ queueChatUpdate(chat, { partial: false, forceName: true });
563
+ }
564
+ });
565
+
566
+ sock.ev.on('chats.update', (updates) => {
567
+ if (!isCurrentSocket()) return;
568
+ for (const update of updates) {
569
+ queueChatUpdate(update, { partial: true });
570
+ }
571
+ });
572
+
573
+ sock.ev.on('chats.delete', (deletions) => {
574
+ if (!isCurrentSocket()) return;
575
+ for (const chatId of deletions) {
576
+ remove('chats', chatId).catch((error) => {
577
+ logger.error('Erro ao remover chat do banco:', {
578
+ error: error.message,
579
+ chatId,
580
+ });
581
+ });
582
+ }
583
+ });
584
+
585
+ sock.ev.on('groups.upsert', async (newGroups) => {
586
+ if (!isCurrentSocket()) return;
587
+ for (const group of newGroups) {
588
+ try {
589
+ await upsertGroupMetadata(group.id, buildGroupMetadataFromGroup(group), {
590
+ mergeExisting: false,
591
+ });
592
+ } catch (error) {
593
+ logger.error('Erro no upsert do grupo:', {
594
+ error: error.message,
595
+ groupId: group.id,
596
+ });
597
+ }
598
+ }
599
+ });
600
+
601
+ sock.ev.on('contacts.update', (updates) => {
602
+ if (!isCurrentSocket()) return;
603
+ if (!Array.isArray(updates)) return;
604
+ for (const update of updates) {
605
+ try {
606
+ const jidCandidate = update?.id || update?.jid || null;
607
+ const lidCandidate = update?.lid || null;
608
+ const jid = isWhatsAppUserId(jidCandidate) ? jidCandidate : null;
609
+ const lid = isLidUserId(lidCandidate) ? lidCandidate : isLidUserId(jidCandidate) ? jidCandidate : null;
610
+ if (lid) {
611
+ queueLidUpdate(lid, jid, 'contacts');
612
+ }
613
+ } catch (error) {
614
+ logger.warn('Falha ao processar contacts.update para lid_map.', { error: error.message });
615
+ }
616
+ }
617
+ });
618
+
619
+ sock.ev.on('lid-mapping.update', (update) => {
620
+ if (!isCurrentSocket()) return;
621
+ try {
622
+ const lid = typeof update?.lid === 'string' ? update.lid : null;
623
+ const pnJid = normalizePnToJid(update?.pn);
624
+ if (!lid || !pnJid) return;
625
+ queueLidUpdate(lid, pnJid, 'lid-mapping');
626
+ } catch (error) {
627
+ logger.warn('Falha ao processar lid-mapping.update para lid_map.', { error: error.message });
628
+ }
629
+ });
630
+
631
+ sock.ev.on('messages.update', (update) => {
632
+ if (!isCurrentSocket()) return;
633
+ try {
634
+ logger.debug('Atualização de mensagens recebida.', {
635
+ action: 'messages_update',
636
+ updatesCount: update.length,
637
+ });
638
+ handleMessageUpdate(update, sock);
639
+ } catch (error) {
640
+ logger.error('Erro no evento messages.update:', {
641
+ error: error.message,
642
+ stack: error.stack,
643
+ action: 'messages_update_error',
644
+ });
645
+ }
646
+ });
647
+
648
+ sock.ev.on('messages.reaction', async (updates) => {
649
+ if (!isCurrentSocket()) return;
650
+ try {
651
+ const reactions = Array.isArray(updates) ? updates : [updates];
652
+ for (const update of reactions) {
653
+ const key = update?.key || update?.msg?.key || update?.reaction?.key || null;
654
+ const reaction = update?.reaction || update?.msg?.reaction || update?.reactionMessage || null;
655
+ const reactedKey = reaction?.key || update?.reactedKey || update?.reactionMessage?.key || null;
656
+
657
+ const groupId = key?.remoteJid || reactedKey?.remoteJid || null;
658
+ const senderJid = key?.participant || update?.participant || reaction?.sender || null;
659
+ const senderIdentity = {
660
+ participant: key?.participant || update?.participant || reaction?.sender || null,
661
+ participantAlt: key?.participantAlt || update?.participantAlt || reaction?.participantAlt || reaction?.key?.participantAlt || null,
662
+ jid: senderJid,
663
+ };
664
+ const reactedMessageId = reactedKey?.id || null;
665
+ const reactionText = typeof reaction?.text === 'string' ? reaction.text : '';
666
+
667
+ if (groupId && (senderJid || senderIdentity.participantAlt)) {
668
+ await resolveCaptchaByReaction({ groupId, senderJid, senderIdentity, reactedMessageId, reactionText });
669
+ }
670
+ }
671
+ } catch (error) {
672
+ logger.error('Erro no evento messages.reaction:', {
673
+ error: error.message,
674
+ stack: error.stack,
675
+ action: 'messages_reaction_error',
676
+ });
677
+ }
678
+ });
679
+
680
+ sock.ev.on('groups.update', (updates) => {
681
+ if (!isCurrentSocket()) return;
682
+ try {
683
+ logger.debug('Grupo(s) atualizado(s).', {
684
+ action: 'groups_update',
685
+ groupCount: updates.length,
686
+ groupIds: updates.map((u) => u.id),
687
+ });
688
+ handleGroupUpdate(updates, sock);
689
+ } catch (err) {
690
+ logger.error('Erro no evento groups.update:', {
691
+ error: err.message,
692
+ stack: err.stack,
693
+ action: 'groups_update_error',
694
+ });
695
+ }
696
+ });
697
+
698
+ sock.ev.on('group-participants.update', (update) => {
699
+ if (!isCurrentSocket()) return;
700
+ try {
701
+ logger.debug('Participantes do grupo atualizados.', {
702
+ action: 'group_participants_update',
703
+ groupId: update.id,
704
+ actionType: update.action,
705
+ participants: update.participants,
706
+ });
707
+ handleGroupParticipantsEvent(sock, update.id, update.participants, update.action);
708
+ } catch (err) {
709
+ logger.error('Erro no evento group-participants.update:', {
710
+ error: err.message,
711
+ stack: err.stack,
712
+ action: 'group_participants_update_error',
713
+ });
714
+ }
715
+ });
716
+
717
+ sock.ev.on('group.join-request', (update) => {
718
+ if (!isCurrentSocket()) return;
719
+ try {
720
+ logger.debug('Solicitação de entrada no grupo recebida.', {
721
+ action: 'group_join_request',
722
+ groupId: update?.id,
723
+ participant: update?.participant,
724
+ method: update?.method,
725
+ joinAction: update?.action,
726
+ });
727
+ handleGroupJoinRequest(sock, update);
728
+ } catch (err) {
729
+ logger.error('Erro no evento group.join-request:', {
730
+ error: err.message,
731
+ stack: err.stack,
732
+ action: 'group_join_request_error',
733
+ });
734
+ }
735
+ });
736
+
737
+ registerBaileysEventLoggers(sock);
738
+
739
+ logger.info('Conexão com o WhatsApp estabelecida com sucesso.', {
740
+ action: 'connect_success',
741
+ generation,
742
+ timestamp: new Date().toISOString(),
743
+ });
744
+ })();
745
+
746
+ try {
747
+ await connectPromise;
748
+ } finally {
749
+ connectPromise = null;
750
+ }
751
+ }
752
+
753
+ /**
754
+ * Gerencia atualizações de estado da conexão com o WhatsApp.
755
+ * Lida com QR code, reconexão automática e ações pós-conexão (sync de grupos).
756
+ * @async
757
+ * @param {import('@whiskeysockets/baileys').ConnectionState} update - Estado da conexão.
758
+ * @param {import('@whiskeysockets/baileys').WASocket} sock - Instância do socket do WhatsApp.
759
+ * @returns {Promise<void>} Conclusão do processamento do estado.
760
+ */
761
+ async function handleConnectionUpdate(update, sock) {
762
+ if (sock !== activeSocket) return;
763
+ const { connection, lastDisconnect, qr } = update;
764
+
765
+ if (qr) {
766
+ logger.info('📱 QR Code gerado! Escaneie com seu WhatsApp.', {
767
+ action: 'qr_code_generated',
768
+ timestamp: new Date().toISOString(),
769
+ });
770
+ qrcode.generate(qr, { small: true });
771
+ }
772
+
773
+ if (connection === 'close') {
774
+ const disconnectCode = lastDisconnect?.error?.output?.statusCode || 'unknown';
775
+ const errorMessage = lastDisconnect?.error?.message || 'Sem mensagem de erro';
776
+
777
+ const shouldReconnect = lastDisconnect?.error instanceof Boom && disconnectCode !== DisconnectReason.loggedOut;
778
+
779
+ if (shouldReconnect) {
780
+ const attempt = getNextReconnectAttempt();
781
+ if (attempt <= MAX_CONNECTION_ATTEMPTS) {
782
+ const reconnectDelay = INITIAL_RECONNECT_DELAY * Math.pow(2, attempt - 1);
783
+ logger.warn(`⚠️ Conexão perdida. Tentando reconectar...`, {
784
+ action: 'reconnect_attempt',
785
+ attempt,
786
+ maxAttempts: MAX_CONNECTION_ATTEMPTS,
787
+ delay: reconnectDelay,
788
+ reasonCode: disconnectCode,
789
+ errorMessage,
790
+ timestamp: new Date().toISOString(),
791
+ });
792
+ activeSocket = null;
793
+ storeActiveSocket(null);
794
+ scheduleReconnect(reconnectDelay);
795
+ } else {
796
+ logger.error('❌ Limite de tentativas atingido; aguardando janela para novo retry.', {
797
+ action: 'reconnect_backoff_window',
798
+ totalAttempts: attempt,
799
+ maxAttempts: MAX_CONNECTION_ATTEMPTS,
800
+ retryAfterMs: BAILEYS_RECONNECT_ATTEMPT_RESET_MS,
801
+ reasonCode: disconnectCode,
802
+ errorMessage,
803
+ timestamp: new Date().toISOString(),
804
+ });
805
+ activeSocket = null;
806
+ storeActiveSocket(null);
807
+ connectionAttempts = 0;
808
+ reconnectWindowStartedAt = Date.now();
809
+ scheduleReconnect(BAILEYS_RECONNECT_ATTEMPT_RESET_MS);
810
+ }
811
+ } else {
812
+ logger.error('❌ Conexão fechada definitivamente.', {
813
+ action: 'connection_closed',
814
+ reasonCode: disconnectCode,
815
+ errorMessage,
816
+ timestamp: new Date().toISOString(),
817
+ });
818
+ }
819
+ }
820
+
821
+ if (connection === 'open') {
822
+ logger.info('✅ Conectado com sucesso ao WhatsApp!', {
823
+ action: 'connection_open',
824
+ timestamp: new Date().toISOString(),
825
+ });
826
+
827
+ resetReconnectState();
828
+ clearReconnectTimeout();
829
+
830
+ if (process.send) {
831
+ process.send('ready');
832
+ logger.info('🟢 Sinal de "ready" enviado ao PM2.', {
833
+ action: 'pm2_ready_signal',
834
+ timestamp: new Date().toISOString(),
835
+ });
836
+ }
837
+
838
+ try {
839
+ await syncGroupsOnConnectionOpen(sock);
840
+ } catch (error) {
841
+ logger.error('❌ Erro ao carregar metadados de grupos na conexão.', {
842
+ action: 'groups_load_error',
843
+ errorMessage: error.message,
844
+ stack: error.stack,
845
+ timeoutMs: GROUP_SYNC_TIMEOUT_MS,
846
+ timestamp: new Date().toISOString(),
847
+ });
848
+ }
849
+ }
850
+ }
851
+
852
+ /**
853
+ * Processa atualizações em mensagens existentes, como votos em enquetes.
854
+ * @async
855
+ * @param {Array<import('@whiskeysockets/baileys').MessageUpdate>} updates - Atualizações de mensagens.
856
+ * @param {import('@whiskeysockets/baileys').WASocket} sock - Instância do socket do WhatsApp.
857
+ * @returns {Promise<void>} Conclusão do processamento das atualizações.
858
+ */
859
+ async function handleMessageUpdate(updates, sock) {
860
+ for (const { key, update } of updates) {
861
+ if (update.pollUpdates) {
862
+ try {
863
+ const pollCreation = await sock.getMessage(key);
864
+
865
+ if (pollCreation) {
866
+ const aggregatedVotes = getAggregateVotesInPollMessage({
867
+ message: pollCreation,
868
+ pollUpdates: update.pollUpdates,
869
+ });
870
+
871
+ logger.info('📊 Votos da enquete atualizados.', {
872
+ action: 'poll_votes_updated',
873
+ remoteJid: key.remoteJid,
874
+ messageId: key.id,
875
+ participant: key.participant || null,
876
+ votesCount: Object.values(aggregatedVotes || {}).reduce((a, b) => a + b, 0),
877
+ votes: aggregatedVotes,
878
+ timestamp: new Date().toISOString(),
879
+ });
880
+ } else {
881
+ logger.warn('⚠️ Mensagem da enquete não encontrada.', {
882
+ action: 'poll_message_not_found',
883
+ key,
884
+ timestamp: new Date().toISOString(),
885
+ });
886
+ }
887
+ } catch (error) {
888
+ logger.error('❌ Erro ao processar atualização de votos da enquete.', {
889
+ action: 'poll_update_error',
890
+ errorMessage: error.message,
891
+ stack: error.stack,
892
+ key,
893
+ timestamp: new Date().toISOString(),
894
+ });
895
+ }
896
+ }
897
+ }
898
+ }
899
+
900
+ /**
901
+ * Atualiza metadados de grupos no banco MySQL a partir dos eventos do Baileys.
902
+ * @async
903
+ * @param {Array<import('@whiskeysockets/baileys').GroupUpdate>} updates - Eventos de atualização de grupos.
904
+ * @param {import('@whiskeysockets/baileys').WASocket} sock - Instância do socket do WhatsApp.
905
+ * @returns {Promise<void>} Conclusão das atualizações em lote.
906
+ * @description
907
+ * Processa alterações de grupo (título, descrição, proprietário e participantes)
908
+ * persistindo a versão consolidada no MySQL.
909
+ */
910
+ async function handleGroupUpdate(updates) {
911
+ await Promise.all(
912
+ updates.map(async (event) => {
913
+ try {
914
+ const groupId = event.id;
915
+ const oldData = (await findById('groups_metadata', groupId)) || {};
916
+ const updatedData = buildGroupMetadataFromUpdate(event, oldData);
917
+
918
+ await upsertGroupMetadata(groupId, updatedData, { mergeExisting: false });
919
+
920
+ const changedFields = Object.keys(event).filter((k) => event[k] !== oldData[k]);
921
+ logger.info('📦 Metadados do grupo atualizados', {
922
+ action: 'group_metadata_updated',
923
+ groupId,
924
+ groupName: updatedData.subject || oldData.subject || 'Desconhecido',
925
+ changedFields,
926
+ timestamp: new Date().toISOString(),
927
+ });
928
+ } catch (error) {
929
+ logger.error('❌ Erro ao atualizar metadados do grupo', {
930
+ action: 'group_metadata_update_error',
931
+ errorMessage: error.message,
932
+ stack: error.stack,
933
+ event,
934
+ timestamp: new Date().toISOString(),
935
+ });
936
+ }
937
+ }),
938
+ );
939
+ }
940
+
941
+ /**
942
+ * Retorna a instância atual do socket ativo do WhatsApp.
943
+ * @returns {import('@whiskeysockets/baileys').WASocket | null} Socket ativo ou null.
944
+ */
945
+ export function getActiveSocket() {
946
+ logger.debug('🔍 Recuperando instância do socket ativo.', {
947
+ action: 'get_active_socket',
948
+ socketExists: !!activeSocket,
949
+ timestamp: new Date().toISOString(),
950
+ });
951
+ return activeSocket;
952
+ }
953
+
954
+ /**
955
+ * Força uma nova tentativa de conexão ao WhatsApp.
956
+ * Encerra o socket atual (se existir) para disparar a lógica de reconexão.
957
+ * @async
958
+ * @returns {Promise<void>} Conclusão do fluxo de reconexão.
959
+ */
960
+ export async function reconnectToWhatsApp() {
961
+ // eslint-disable-next-line no-undef
962
+ if (activeSocket && activeSocket.ws?.readyState === WebSocket.OPEN) {
963
+ logger.info('♻️ Forçando fechamento do socket para reconectar...', {
964
+ action: 'force_reconnect',
965
+ timestamp: new Date().toISOString(),
966
+ });
967
+ activeSocket.ws.close();
968
+ } else {
969
+ logger.warn('⚠️ Nenhum socket ativo detectado. Iniciando nova conexão manualmente.', {
970
+ action: 'reconnect_no_active_socket',
971
+ timestamp: new Date().toISOString(),
972
+ });
973
+ await connectToWhatsApp();
974
+ }
975
+ }
976
+
977
+ if (process.argv[1] === __filename) {
978
+ logger.info('🚀 Socket Controller iniciado diretamente via CLI.', {
979
+ action: 'module_direct_execution',
980
+ timestamp: new Date().toISOString(),
981
+ });
982
+
983
+ connectToWhatsApp().catch((err) => {
984
+ logger.error('❌ Falha crítica ao tentar iniciar conexão via execução direta.', {
985
+ action: 'direct_connection_failure',
986
+ errorMessage: err.message,
987
+ stack: err.stack,
988
+ timestamp: new Date().toISOString(),
989
+ });
990
+ process.exit(1);
991
+ });
992
+ }