@omnizap-system/omnizap 2.6.1 → 2.6.2

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 (156) hide show
  1. package/.env.example +54 -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 +2 -0
  6. package/app/configParts/adminIdentity.js +5 -5
  7. package/app/configParts/baileysConfig.js +226 -55
  8. package/app/configParts/groupUtils.js +5 -0
  9. package/app/configParts/messagePersistenceService.js +143 -3
  10. package/app/configParts/sessionConfig.js +157 -0
  11. package/app/connection/baileysCompatibility.test.js +1 -1
  12. package/app/connection/groupOwnerWriteStateResolver.js +109 -0
  13. package/app/connection/socketController.js +625 -124
  14. package/app/connection/socketController.multiSession.test.js +108 -0
  15. package/app/controllers/messageController.js +1 -1
  16. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  17. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  18. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  19. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
  20. package/app/controllers/messageProcessingPipeline.js +88 -9
  21. package/app/controllers/messageProcessingPipeline.test.js +200 -0
  22. package/app/modules/adminModule/AGENT.md +1 -1
  23. package/app/modules/adminModule/commandConfig.json +3318 -1347
  24. package/app/modules/adminModule/groupCommandHandlers.js +856 -14
  25. package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
  26. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  27. package/app/modules/aiModule/AGENT.md +47 -30
  28. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  29. package/app/modules/aiModule/catCommand.js +132 -25
  30. package/app/modules/aiModule/commandConfig.json +114 -28
  31. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  32. package/app/modules/gameModule/AGENT.md +1 -1
  33. package/app/modules/gameModule/commandConfig.json +29 -0
  34. package/app/modules/menuModule/AGENT.md +1 -1
  35. package/app/modules/menuModule/commandConfig.json +45 -10
  36. package/app/modules/menuModule/menuCatalogService.js +190 -0
  37. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  38. package/app/modules/menuModule/menuDynamicService.js +511 -0
  39. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  40. package/app/modules/menuModule/menus.js +36 -5
  41. package/app/modules/playModule/AGENT.md +10 -5
  42. package/app/modules/playModule/commandConfig.json +74 -16
  43. package/app/modules/playModule/playCommandConstants.js +13 -7
  44. package/app/modules/playModule/playCommandCore.js +4 -6
  45. package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
  46. package/app/modules/playModule/playConfigRuntime.js +5 -6
  47. package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
  48. package/app/modules/quoteModule/AGENT.md +1 -1
  49. package/app/modules/quoteModule/commandConfig.json +29 -0
  50. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  51. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  52. package/app/modules/statsModule/AGENT.md +1 -1
  53. package/app/modules/statsModule/commandConfig.json +58 -0
  54. package/app/modules/stickerModule/AGENT.md +1 -1
  55. package/app/modules/stickerModule/commandConfig.json +145 -0
  56. package/app/modules/stickerPackModule/AGENT.md +1 -1
  57. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  58. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  59. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
  60. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
  61. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  62. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  63. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  64. package/app/modules/tiktokModule/AGENT.md +1 -1
  65. package/app/modules/tiktokModule/commandConfig.json +29 -0
  66. package/app/modules/userModule/AGENT.md +1 -1
  67. package/app/modules/userModule/commandConfig.json +29 -0
  68. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  69. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  70. package/app/observability/metrics.js +136 -0
  71. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  72. package/app/services/ai/geminiService.js +131 -7
  73. package/app/services/ai/geminiService.test.js +59 -2
  74. package/app/services/ai/moduleAiHelpCoreService.js +33 -4
  75. package/app/services/group/groupMetadataService.js +24 -1
  76. package/app/services/infra/dbWriteQueue.js +51 -21
  77. package/app/services/messaging/newsBroadcastService.js +843 -27
  78. package/app/services/multiSession/assignmentBalancerService.js +457 -0
  79. package/app/services/multiSession/groupOwnershipRepository.js +381 -0
  80. package/app/services/multiSession/groupOwnershipService.js +890 -0
  81. package/app/services/multiSession/groupOwnershipService.test.js +309 -0
  82. package/app/services/multiSession/sessionRegistryService.js +293 -0
  83. package/app/store/aiPromptStore.js +36 -19
  84. package/app/store/groupConfigStore.js +41 -5
  85. package/app/store/premiumUserStore.js +21 -7
  86. package/app/utils/antiLink/antiLinkModule.js +352 -16
  87. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  88. package/database/index.js +6 -0
  89. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  90. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  91. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  92. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  93. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  94. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  95. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  96. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  97. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  98. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  99. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  100. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  101. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  102. package/database/schema.sql +102 -1
  103. package/docker-compose.yml +4 -1
  104. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  105. package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
  106. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  107. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  108. package/docs/security/omnizap-static-security-headers.conf +25 -0
  109. package/ecosystem.prod.config.cjs +31 -11
  110. package/index.js +52 -18
  111. package/observability/alert-rules.yml +20 -0
  112. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  113. package/observability/mysql-setup.sql +4 -4
  114. package/observability/system-admin-observability.md +26 -0
  115. package/package.json +12 -5
  116. package/public/comandos/commands-catalog.json +2253 -78
  117. package/public/js/apps/commandsReactApp.js +267 -87
  118. package/public/js/apps/createPackApp.js +3 -3
  119. package/public/js/apps/stickersApp.js +255 -103
  120. package/public/js/apps/termsReactApp.js +57 -8
  121. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  122. package/public/js/apps/userReactApp.js +96 -47
  123. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  124. package/public/pages/politica-de-privacidade.html +1 -1
  125. package/public/pages/stickers.html +5 -5
  126. package/public/pages/termos-de-uso-texto-integral.html +1 -1
  127. package/public/pages/termos-de-uso.html +1 -1
  128. package/public/pages/user-password-reset.html +3 -4
  129. package/public/pages/user-systemadm.html +8 -462
  130. package/public/pages/user.html +1 -1
  131. package/scripts/clear-whatsapp-session.sh +123 -0
  132. package/scripts/core-ai-mode.mjs +163 -0
  133. package/scripts/deploy.sh +10 -0
  134. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  135. package/scripts/generate-commands-catalog.mjs +155 -0
  136. package/scripts/new-whatsapp-session.sh +317 -0
  137. package/scripts/security-web-surface-check.mjs +218 -0
  138. package/server/controllers/admin/adminPanelHandlers.js +253 -3
  139. package/server/controllers/admin/systemAdminController.js +267 -0
  140. package/server/controllers/sticker/stickerCatalogController.js +9 -23
  141. package/server/controllers/system/contactController.js +9 -17
  142. package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
  143. package/server/controllers/system/systemController.js +254 -1
  144. package/server/controllers/userController.js +6 -0
  145. package/server/email/emailTemplateService.js +3 -2
  146. package/server/http/httpServer.js +8 -4
  147. package/server/middleware/securityHeaders.js +20 -1
  148. package/server/routes/admin/systemAdminRouter.js +6 -0
  149. package/server/routes/indexRouter.js +30 -6
  150. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  151. package/server/routes/static/staticPageRouter.js +27 -1
  152. package/server/utils/publicContact.js +31 -0
  153. package/utils/whatsapp/contactEnv.js +39 -0
  154. package/vite.config.mjs +2 -1
  155. package/app/modules/playModule/local/installYtDlp.js +0 -25
  156. package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
@@ -0,0 +1,32 @@
1
+ -- D+36 rollback
2
+
3
+ SET @migration_key := '20260412_d36_system_config_tables';
4
+
5
+ INSERT INTO `group_configs` (`id`, `config`, `updated_at`)
6
+ SELECT
7
+ 'system:premium_users' AS `id`,
8
+ JSON_OBJECT('premiumUsers', COALESCE(JSON_ARRAYAGG(`id`), JSON_ARRAY())) AS `config`,
9
+ CURRENT_TIMESTAMP AS `updated_at`
10
+ FROM `system_premium_users`
11
+ ON DUPLICATE KEY UPDATE
12
+ `config` = VALUES(`config`),
13
+ `updated_at` = CURRENT_TIMESTAMP;
14
+
15
+ INSERT INTO `group_configs` (`id`, `config`, `updated_at`)
16
+ SELECT
17
+ 'system:ai_prompts' AS `id`,
18
+ JSON_OBJECT('prompts', COALESCE(JSON_OBJECTAGG(`id`, `prompt`), JSON_OBJECT())) AS `config`,
19
+ CURRENT_TIMESTAMP AS `updated_at`
20
+ FROM `system_ai_prompts`
21
+ ON DUPLICATE KEY UPDATE
22
+ `config` = VALUES(`config`),
23
+ `updated_at` = CURRENT_TIMESTAMP;
24
+
25
+ DROP TABLE IF EXISTS `system_ai_prompts`;
26
+ DROP TABLE IF EXISTS `system_premium_users`;
27
+
28
+ UPDATE schema_change_log
29
+ SET status = 'rolled_back',
30
+ notes = 'D+36 rollback executed',
31
+ updated_at = CURRENT_TIMESTAMP
32
+ WHERE migration_key = @migration_key;
@@ -0,0 +1,66 @@
1
+ -- D+36 (2026-04-12) - Split system config records from group_configs
2
+ -- Scope: move `system:premium_users` and `system:ai_prompts` to dedicated tables
3
+
4
+ SET @migration_key := '20260412_d36_system_config_tables';
5
+
6
+ CREATE TABLE IF NOT EXISTS `system_premium_users` (
7
+ `id` varchar(255) NOT NULL,
8
+ `created_at` timestamp NULL DEFAULT current_timestamp(),
9
+ `updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
10
+ PRIMARY KEY (`id`)
11
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
12
+
13
+ CREATE TABLE IF NOT EXISTS `system_ai_prompts` (
14
+ `id` varchar(255) NOT NULL,
15
+ `prompt` longtext NOT NULL,
16
+ `created_at` timestamp NULL DEFAULT current_timestamp(),
17
+ `updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
18
+ PRIMARY KEY (`id`)
19
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
20
+
21
+ INSERT INTO `system_premium_users` (`id`)
22
+ SELECT DISTINCT jt.premium_jid
23
+ FROM `group_configs` gc
24
+ JOIN JSON_TABLE(
25
+ COALESCE(JSON_EXTRACT(gc.config, '$.premiumUsers'), JSON_ARRAY()),
26
+ '$[*]' COLUMNS (`premium_jid` VARCHAR(255) PATH '$')
27
+ ) jt
28
+ WHERE gc.id = 'system:premium_users'
29
+ AND jt.premium_jid IS NOT NULL
30
+ AND TRIM(jt.premium_jid) <> ''
31
+ ON DUPLICATE KEY UPDATE
32
+ `updated_at` = CURRENT_TIMESTAMP;
33
+
34
+ INSERT INTO `system_ai_prompts` (`id`, `prompt`)
35
+ SELECT jt.prompt_jid,
36
+ COALESCE(
37
+ JSON_UNQUOTE(
38
+ JSON_EXTRACT(
39
+ JSON_EXTRACT(gc.config, '$.prompts'),
40
+ CONCAT('$."', REPLACE(jt.prompt_jid, '"', '\\"'), '"')
41
+ )
42
+ ),
43
+ ''
44
+ ) AS prompt_text
45
+ FROM `group_configs` gc
46
+ JOIN JSON_TABLE(
47
+ JSON_KEYS(COALESCE(JSON_EXTRACT(gc.config, '$.prompts'), JSON_OBJECT())),
48
+ '$[*]' COLUMNS (`prompt_jid` VARCHAR(255) PATH '$')
49
+ ) jt
50
+ WHERE gc.id = 'system:ai_prompts'
51
+ AND jt.prompt_jid IS NOT NULL
52
+ AND TRIM(jt.prompt_jid) <> ''
53
+ ON DUPLICATE KEY UPDATE
54
+ `prompt` = VALUES(`prompt`),
55
+ `updated_at` = CURRENT_TIMESTAMP;
56
+
57
+ DELETE FROM `group_configs`
58
+ WHERE id IN ('system:premium_users', 'system:ai_prompts');
59
+
60
+ INSERT INTO schema_change_log (migration_key, phase, status, notes)
61
+ VALUES (@migration_key, 'D+36', 'applied', 'split system:premium_users and system:ai_prompts into dedicated tables')
62
+ ON DUPLICATE KEY UPDATE
63
+ phase = VALUES(phase),
64
+ status = 'applied',
65
+ notes = VALUES(notes),
66
+ updated_at = CURRENT_TIMESTAMP;
@@ -0,0 +1,11 @@
1
+ -- D+37 rollback
2
+
3
+ SET @migration_key := '20260413_d37_group_user_warnings';
4
+
5
+ DROP TABLE IF EXISTS `group_user_warnings`;
6
+
7
+ UPDATE schema_change_log
8
+ SET status = 'rolled_back',
9
+ notes = 'D+37 rollback executed',
10
+ updated_at = CURRENT_TIMESTAMP
11
+ WHERE migration_key = @migration_key;
@@ -0,0 +1,24 @@
1
+ -- D+37 (2026-04-13) - Group moderation warnings
2
+ -- Scope: track warnings per participant inside each group
3
+
4
+ SET @migration_key := '20260413_d37_group_user_warnings';
5
+
6
+ CREATE TABLE IF NOT EXISTS `group_user_warnings` (
7
+ `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
8
+ `group_id` varchar(255) NOT NULL,
9
+ `participant_jid` varchar(255) NOT NULL,
10
+ `warned_by_jid` varchar(255) DEFAULT NULL,
11
+ `reason` text DEFAULT NULL,
12
+ `created_at` timestamp NULL DEFAULT current_timestamp(),
13
+ PRIMARY KEY (`id`),
14
+ KEY `idx_group_user_warnings_lookup` (`group_id`,`participant_jid`,`created_at`),
15
+ KEY `idx_group_user_warnings_prune` (`group_id`,`participant_jid`,`id`)
16
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
17
+
18
+ INSERT INTO schema_change_log (migration_key, phase, status, notes)
19
+ VALUES (@migration_key, 'D+37', 'applied', 'created group_user_warnings table for moderation warnings')
20
+ ON DUPLICATE KEY UPDATE
21
+ phase = VALUES(phase),
22
+ status = 'applied',
23
+ notes = VALUES(notes),
24
+ updated_at = CURRENT_TIMESTAMP;
@@ -0,0 +1,72 @@
1
+ -- D+38 rollback
2
+
3
+ SET @migration_key := '20260414_d38_multi_session_foundation';
4
+
5
+ DROP PROCEDURE IF EXISTS __drop_column_if_exists;
6
+ DROP PROCEDURE IF EXISTS __drop_index_if_exists;
7
+
8
+ DELIMITER $$
9
+ CREATE PROCEDURE __drop_column_if_exists(IN p_table_name VARCHAR(64), IN p_column_name VARCHAR(64))
10
+ BEGIN
11
+ IF EXISTS (
12
+ SELECT 1
13
+ FROM information_schema.columns
14
+ WHERE table_schema = DATABASE()
15
+ AND table_name = p_table_name
16
+ AND column_name = p_column_name
17
+ ) THEN
18
+ SET @ddl = CONCAT('ALTER TABLE `', p_table_name, '` DROP COLUMN `', p_column_name, '`');
19
+ PREPARE stmt FROM @ddl;
20
+ EXECUTE stmt;
21
+ DEALLOCATE PREPARE stmt;
22
+ END IF;
23
+ END$$
24
+
25
+ CREATE PROCEDURE __drop_index_if_exists(IN p_table_name VARCHAR(64), IN p_index_name VARCHAR(64))
26
+ BEGIN
27
+ IF EXISTS (
28
+ SELECT 1
29
+ FROM information_schema.statistics
30
+ WHERE table_schema = DATABASE()
31
+ AND table_name = p_table_name
32
+ AND index_name = p_index_name
33
+ ) THEN
34
+ SET @ddl = CONCAT('ALTER TABLE `', p_table_name, '` DROP INDEX `', p_index_name, '`');
35
+ PREPARE stmt FROM @ddl;
36
+ EXECUTE stmt;
37
+ DEALLOCATE PREPARE stmt;
38
+ END IF;
39
+ END$$
40
+ DELIMITER ;
41
+
42
+ CALL __drop_index_if_exists('message_analysis_event', 'idx_message_analysis_session_command_created');
43
+ CALL __drop_index_if_exists('message_analysis_event', 'idx_message_analysis_session_sender_created');
44
+ CALL __drop_index_if_exists('message_analysis_event', 'idx_message_analysis_session_chat_created');
45
+ CALL __drop_index_if_exists('message_analysis_event', 'idx_message_analysis_session_created');
46
+
47
+ CALL __drop_index_if_exists('baileys_event_journal', 'idx_baileys_event_session_message_created');
48
+ CALL __drop_index_if_exists('baileys_event_journal', 'idx_baileys_event_session_chat_created');
49
+ CALL __drop_index_if_exists('baileys_event_journal', 'idx_baileys_event_session_name_created');
50
+ CALL __drop_index_if_exists('baileys_event_journal', 'idx_baileys_event_session_created');
51
+
52
+ CALL __drop_index_if_exists('messages', 'idx_messages_session_canonical_sender_timestamp');
53
+ CALL __drop_index_if_exists('messages', 'idx_messages_session_sender_timestamp');
54
+ CALL __drop_index_if_exists('messages', 'idx_messages_session_chat_timestamp');
55
+ CALL __drop_index_if_exists('messages', 'idx_messages_session_message_id');
56
+
57
+ CALL __drop_column_if_exists('message_analysis_event', 'session_id');
58
+ CALL __drop_column_if_exists('baileys_event_journal', 'session_id');
59
+ CALL __drop_column_if_exists('messages', 'session_id');
60
+
61
+ DROP TABLE IF EXISTS `group_assignment_history`;
62
+ DROP TABLE IF EXISTS `group_assignment`;
63
+ DROP TABLE IF EXISTS `wa_session_registry`;
64
+
65
+ UPDATE schema_change_log
66
+ SET status = 'rolled_back',
67
+ notes = 'D+38 rollback executed',
68
+ updated_at = CURRENT_TIMESTAMP
69
+ WHERE migration_key = @migration_key;
70
+
71
+ DROP PROCEDURE IF EXISTS __drop_column_if_exists;
72
+ DROP PROCEDURE IF EXISTS __drop_index_if_exists;
@@ -0,0 +1,125 @@
1
+ -- D+38 (2026-04-14) - Multi-session foundation
2
+ -- Scope:
3
+ -- 1) add group ownership/session registry tables
4
+ -- 2) add session_id scoping columns to high-traffic WhatsApp tables
5
+ -- 3) add session-aware composite indexes
6
+
7
+ SET @migration_key := '20260414_d38_multi_session_foundation';
8
+
9
+ DROP PROCEDURE IF EXISTS __ensure_column;
10
+ DROP PROCEDURE IF EXISTS __ensure_index;
11
+
12
+ DELIMITER $$
13
+ CREATE PROCEDURE __ensure_column(IN p_table_name VARCHAR(64), IN p_column_name VARCHAR(64), IN p_ddl TEXT)
14
+ BEGIN
15
+ IF NOT EXISTS (
16
+ SELECT 1
17
+ FROM information_schema.columns
18
+ WHERE table_schema = DATABASE()
19
+ AND table_name = p_table_name
20
+ AND column_name = p_column_name
21
+ ) THEN
22
+ SET @ddl = p_ddl;
23
+ PREPARE stmt FROM @ddl;
24
+ EXECUTE stmt;
25
+ DEALLOCATE PREPARE stmt;
26
+ END IF;
27
+ END$$
28
+
29
+ CREATE PROCEDURE __ensure_index(IN p_table_name VARCHAR(64), IN p_index_name VARCHAR(64), IN p_ddl TEXT)
30
+ BEGIN
31
+ IF NOT EXISTS (
32
+ SELECT 1
33
+ FROM information_schema.statistics
34
+ WHERE table_schema = DATABASE()
35
+ AND table_name = p_table_name
36
+ AND index_name = p_index_name
37
+ ) THEN
38
+ SET @ddl = p_ddl;
39
+ PREPARE stmt FROM @ddl;
40
+ EXECUTE stmt;
41
+ DEALLOCATE PREPARE stmt;
42
+ END IF;
43
+ END$$
44
+ DELIMITER ;
45
+
46
+ CREATE TABLE IF NOT EXISTS `wa_session_registry` (
47
+ `session_id` varchar(64) NOT NULL,
48
+ `bot_jid` varchar(255) DEFAULT NULL,
49
+ `status` varchar(24) NOT NULL DEFAULT 'offline',
50
+ `capacity_weight` int(10) unsigned NOT NULL DEFAULT 1,
51
+ `current_score` decimal(12,4) NOT NULL DEFAULT 0.0000,
52
+ `last_heartbeat_at` datetime DEFAULT NULL,
53
+ `last_connected_at` datetime DEFAULT NULL,
54
+ `last_disconnected_at` datetime DEFAULT NULL,
55
+ `metadata` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`metadata`)),
56
+ `created_at` timestamp NOT NULL DEFAULT current_timestamp(),
57
+ `updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
58
+ PRIMARY KEY (`session_id`),
59
+ KEY `idx_wa_session_registry_status_updated` (`status`,`updated_at`),
60
+ KEY `idx_wa_session_registry_heartbeat` (`last_heartbeat_at`)
61
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
62
+
63
+ CREATE TABLE IF NOT EXISTS `group_assignment` (
64
+ `group_jid` varchar(255) NOT NULL,
65
+ `owner_session_id` varchar(64) NOT NULL,
66
+ `lease_expires_at` datetime NOT NULL,
67
+ `cooldown_until` datetime DEFAULT NULL,
68
+ `assignment_version` bigint(20) unsigned NOT NULL DEFAULT 1,
69
+ `pinned` tinyint(1) NOT NULL DEFAULT 0,
70
+ `last_reason` varchar(64) DEFAULT NULL,
71
+ `created_at` timestamp NOT NULL DEFAULT current_timestamp(),
72
+ `updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
73
+ PRIMARY KEY (`group_jid`),
74
+ KEY `idx_group_assignment_owner_lease` (`owner_session_id`,`lease_expires_at`),
75
+ KEY `idx_group_assignment_lease` (`lease_expires_at`),
76
+ KEY `idx_group_assignment_cooldown` (`cooldown_until`),
77
+ KEY `idx_group_assignment_pinned_updated` (`pinned`,`updated_at`),
78
+ CONSTRAINT `fk_group_assignment_owner_session` FOREIGN KEY (`owner_session_id`) REFERENCES `wa_session_registry` (`session_id`) ON UPDATE CASCADE
79
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
80
+
81
+ CREATE TABLE IF NOT EXISTS `group_assignment_history` (
82
+ `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
83
+ `group_jid` varchar(255) NOT NULL,
84
+ `previous_session_id` varchar(64) DEFAULT NULL,
85
+ `new_session_id` varchar(64) NOT NULL,
86
+ `change_reason` varchar(64) DEFAULT NULL,
87
+ `changed_by` varchar(64) NOT NULL DEFAULT 'system',
88
+ `assignment_version` bigint(20) unsigned NOT NULL,
89
+ `metadata` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`metadata`)),
90
+ `created_at` timestamp NOT NULL DEFAULT current_timestamp(),
91
+ PRIMARY KEY (`id`),
92
+ KEY `idx_group_assignment_history_group_created` (`group_jid`,`created_at`),
93
+ KEY `idx_group_assignment_history_new_session_created` (`new_session_id`,`created_at`),
94
+ KEY `idx_group_assignment_history_prev_session_created` (`previous_session_id`,`created_at`)
95
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
96
+
97
+ CALL __ensure_column('messages', 'session_id', 'ALTER TABLE messages ADD COLUMN session_id VARCHAR(64) NOT NULL DEFAULT ''default'' AFTER message_id');
98
+ CALL __ensure_column('baileys_event_journal', 'session_id', 'ALTER TABLE baileys_event_journal ADD COLUMN session_id VARCHAR(64) NOT NULL DEFAULT ''default'' AFTER id');
99
+ CALL __ensure_column('message_analysis_event', 'session_id', 'ALTER TABLE message_analysis_event ADD COLUMN session_id VARCHAR(64) NOT NULL DEFAULT ''default'' AFTER id');
100
+
101
+ CALL __ensure_index('messages', 'idx_messages_session_message_id', 'CREATE INDEX idx_messages_session_message_id ON messages (session_id, message_id)');
102
+ CALL __ensure_index('messages', 'idx_messages_session_chat_timestamp', 'CREATE INDEX idx_messages_session_chat_timestamp ON messages (session_id, chat_id, timestamp)');
103
+ CALL __ensure_index('messages', 'idx_messages_session_sender_timestamp', 'CREATE INDEX idx_messages_session_sender_timestamp ON messages (session_id, sender_id, timestamp)');
104
+ CALL __ensure_index('messages', 'idx_messages_session_canonical_sender_timestamp', 'CREATE INDEX idx_messages_session_canonical_sender_timestamp ON messages (session_id, canonical_sender_id, timestamp)');
105
+
106
+ CALL __ensure_index('baileys_event_journal', 'idx_baileys_event_session_created', 'CREATE INDEX idx_baileys_event_session_created ON baileys_event_journal (session_id, created_at)');
107
+ CALL __ensure_index('baileys_event_journal', 'idx_baileys_event_session_name_created', 'CREATE INDEX idx_baileys_event_session_name_created ON baileys_event_journal (session_id, event_name, created_at)');
108
+ CALL __ensure_index('baileys_event_journal', 'idx_baileys_event_session_chat_created', 'CREATE INDEX idx_baileys_event_session_chat_created ON baileys_event_journal (session_id, chat_id, created_at)');
109
+ CALL __ensure_index('baileys_event_journal', 'idx_baileys_event_session_message_created', 'CREATE INDEX idx_baileys_event_session_message_created ON baileys_event_journal (session_id, message_id, created_at)');
110
+
111
+ CALL __ensure_index('message_analysis_event', 'idx_message_analysis_session_created', 'CREATE INDEX idx_message_analysis_session_created ON message_analysis_event (session_id, created_at)');
112
+ CALL __ensure_index('message_analysis_event', 'idx_message_analysis_session_chat_created', 'CREATE INDEX idx_message_analysis_session_chat_created ON message_analysis_event (session_id, chat_id, created_at)');
113
+ CALL __ensure_index('message_analysis_event', 'idx_message_analysis_session_sender_created', 'CREATE INDEX idx_message_analysis_session_sender_created ON message_analysis_event (session_id, sender_id, created_at)');
114
+ CALL __ensure_index('message_analysis_event', 'idx_message_analysis_session_command_created', 'CREATE INDEX idx_message_analysis_session_command_created ON message_analysis_event (session_id, command_name, created_at)');
115
+
116
+ INSERT INTO schema_change_log (migration_key, phase, status, notes)
117
+ VALUES (@migration_key, 'D+38', 'applied', 'multi-session foundation tables and session-scoped indexes')
118
+ ON DUPLICATE KEY UPDATE
119
+ phase = VALUES(phase),
120
+ status = 'applied',
121
+ notes = VALUES(notes),
122
+ updated_at = CURRENT_TIMESTAMP;
123
+
124
+ DROP PROCEDURE IF EXISTS __ensure_column;
125
+ DROP PROCEDURE IF EXISTS __ensure_index;
@@ -0,0 +1,103 @@
1
+ -- D+39 rollback
2
+
3
+ SET @migration_key := '20260414_d39_multi_session_cutover';
4
+
5
+ DROP PROCEDURE IF EXISTS __drop_index_if_exists;
6
+ DROP PROCEDURE IF EXISTS __ensure_unique_index;
7
+ DROP PROCEDURE IF EXISTS __ensure_index;
8
+ DROP PROCEDURE IF EXISTS __assert_no_global_message_duplicates;
9
+
10
+ DELIMITER $$
11
+ CREATE PROCEDURE __drop_index_if_exists(IN p_table_name VARCHAR(64), IN p_index_name VARCHAR(64))
12
+ BEGIN
13
+ IF EXISTS (
14
+ SELECT 1
15
+ FROM information_schema.statistics
16
+ WHERE table_schema = DATABASE()
17
+ AND table_name = p_table_name
18
+ AND index_name = p_index_name
19
+ ) THEN
20
+ SET @ddl = CONCAT('ALTER TABLE `', p_table_name, '` DROP INDEX `', p_index_name, '`');
21
+ PREPARE stmt FROM @ddl;
22
+ EXECUTE stmt;
23
+ DEALLOCATE PREPARE stmt;
24
+ END IF;
25
+ END$$
26
+
27
+ CREATE PROCEDURE __ensure_unique_index(IN p_table_name VARCHAR(64), IN p_index_name VARCHAR(64), IN p_ddl TEXT)
28
+ BEGIN
29
+ IF NOT EXISTS (
30
+ SELECT 1
31
+ FROM information_schema.statistics
32
+ WHERE table_schema = DATABASE()
33
+ AND table_name = p_table_name
34
+ AND index_name = p_index_name
35
+ AND non_unique = 0
36
+ ) THEN
37
+ SET @ddl = p_ddl;
38
+ PREPARE stmt FROM @ddl;
39
+ EXECUTE stmt;
40
+ DEALLOCATE PREPARE stmt;
41
+ END IF;
42
+ END$$
43
+
44
+ CREATE PROCEDURE __ensure_index(IN p_table_name VARCHAR(64), IN p_index_name VARCHAR(64), IN p_ddl TEXT)
45
+ BEGIN
46
+ IF NOT EXISTS (
47
+ SELECT 1
48
+ FROM information_schema.statistics
49
+ WHERE table_schema = DATABASE()
50
+ AND table_name = p_table_name
51
+ AND index_name = p_index_name
52
+ ) THEN
53
+ SET @ddl = p_ddl;
54
+ PREPARE stmt FROM @ddl;
55
+ EXECUTE stmt;
56
+ DEALLOCATE PREPARE stmt;
57
+ END IF;
58
+ END$$
59
+
60
+ CREATE PROCEDURE __assert_no_global_message_duplicates()
61
+ BEGIN
62
+ DECLARE v_dup_exists INT DEFAULT 0;
63
+
64
+ SELECT
65
+ CASE
66
+ WHEN EXISTS (
67
+ SELECT 1
68
+ FROM messages
69
+ GROUP BY message_id
70
+ HAVING COUNT(*) > 1
71
+ LIMIT 1
72
+ ) THEN 1
73
+ ELSE 0
74
+ END
75
+ INTO v_dup_exists;
76
+
77
+ IF v_dup_exists = 1 THEN
78
+ SIGNAL SQLSTATE '45000'
79
+ SET MESSAGE_TEXT = 'Rollback D+39 bloqueado: existem message_id duplicados entre sessoes.';
80
+ END IF;
81
+ END$$
82
+ DELIMITER ;
83
+
84
+ CALL __assert_no_global_message_duplicates();
85
+
86
+ CALL __drop_index_if_exists('messages', 'uq_messages_session_message_id');
87
+
88
+ -- Retorna ao estado da fase D+38:
89
+ -- 1) índice único global em message_id
90
+ -- 2) índice não-único de apoio em (session_id, message_id)
91
+ CALL __ensure_unique_index('messages', 'message_id', 'CREATE UNIQUE INDEX message_id ON messages (message_id)');
92
+ CALL __ensure_index('messages', 'idx_messages_session_message_id', 'CREATE INDEX idx_messages_session_message_id ON messages (session_id, message_id)');
93
+
94
+ UPDATE schema_change_log
95
+ SET status = 'rolled_back',
96
+ notes = 'D+39 rollback executed',
97
+ updated_at = CURRENT_TIMESTAMP
98
+ WHERE migration_key = @migration_key;
99
+
100
+ DROP PROCEDURE IF EXISTS __drop_index_if_exists;
101
+ DROP PROCEDURE IF EXISTS __ensure_unique_index;
102
+ DROP PROCEDURE IF EXISTS __ensure_index;
103
+ DROP PROCEDURE IF EXISTS __assert_no_global_message_duplicates;
@@ -0,0 +1,83 @@
1
+ -- D+39 (2026-04-14) - Multi-session message uniqueness cutover
2
+ -- Scope: replace global unique(message_id) with unique(session_id, message_id)
3
+
4
+ SET @migration_key := '20260414_d39_multi_session_cutover';
5
+
6
+ DROP PROCEDURE IF EXISTS __assert_column_exists;
7
+ DROP PROCEDURE IF EXISTS __drop_index_if_exists;
8
+ DROP PROCEDURE IF EXISTS __ensure_unique_index;
9
+
10
+ DELIMITER $$
11
+ CREATE PROCEDURE __assert_column_exists(IN p_table_name VARCHAR(64), IN p_column_name VARCHAR(64))
12
+ BEGIN
13
+ IF NOT EXISTS (
14
+ SELECT 1
15
+ FROM information_schema.columns
16
+ WHERE table_schema = DATABASE()
17
+ AND table_name = p_table_name
18
+ AND column_name = p_column_name
19
+ ) THEN
20
+ SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'D+39 requer coluna session_id previamente criada (execute D+38 antes).';
21
+ END IF;
22
+ END$$
23
+
24
+ CREATE PROCEDURE __drop_index_if_exists(IN p_table_name VARCHAR(64), IN p_index_name VARCHAR(64))
25
+ BEGIN
26
+ IF EXISTS (
27
+ SELECT 1
28
+ FROM information_schema.statistics
29
+ WHERE table_schema = DATABASE()
30
+ AND table_name = p_table_name
31
+ AND index_name = p_index_name
32
+ ) THEN
33
+ SET @ddl = CONCAT('ALTER TABLE `', p_table_name, '` DROP INDEX `', p_index_name, '`');
34
+ PREPARE stmt FROM @ddl;
35
+ EXECUTE stmt;
36
+ DEALLOCATE PREPARE stmt;
37
+ END IF;
38
+ END$$
39
+
40
+ CREATE PROCEDURE __ensure_unique_index(IN p_table_name VARCHAR(64), IN p_index_name VARCHAR(64), IN p_ddl TEXT)
41
+ BEGIN
42
+ IF NOT EXISTS (
43
+ SELECT 1
44
+ FROM information_schema.statistics
45
+ WHERE table_schema = DATABASE()
46
+ AND table_name = p_table_name
47
+ AND index_name = p_index_name
48
+ AND non_unique = 0
49
+ ) THEN
50
+ SET @ddl = p_ddl;
51
+ PREPARE stmt FROM @ddl;
52
+ EXECUTE stmt;
53
+ DEALLOCATE PREPARE stmt;
54
+ END IF;
55
+ END$$
56
+ DELIMITER ;
57
+
58
+ CALL __assert_column_exists('messages', 'session_id');
59
+
60
+ -- Remove índice legado global e índice não-único redundante da fase D+38.
61
+ CALL __drop_index_if_exists('messages', 'message_id');
62
+ CALL __drop_index_if_exists('messages', 'uq_messages_message_id');
63
+ CALL __drop_index_if_exists('messages', 'uniq_messages_message_id');
64
+ CALL __drop_index_if_exists('messages', 'idx_messages_session_message_id');
65
+
66
+ -- Novo contrato de unicidade por sessão.
67
+ CALL __ensure_unique_index(
68
+ 'messages',
69
+ 'uq_messages_session_message_id',
70
+ 'CREATE UNIQUE INDEX uq_messages_session_message_id ON messages (session_id, message_id)'
71
+ );
72
+
73
+ INSERT INTO schema_change_log (migration_key, phase, status, notes)
74
+ VALUES (@migration_key, 'D+39', 'applied', 'messages uniqueness changed to (session_id, message_id)')
75
+ ON DUPLICATE KEY UPDATE
76
+ phase = VALUES(phase),
77
+ status = 'applied',
78
+ notes = VALUES(notes),
79
+ updated_at = CURRENT_TIMESTAMP;
80
+
81
+ DROP PROCEDURE IF EXISTS __assert_column_exists;
82
+ DROP PROCEDURE IF EXISTS __drop_index_if_exists;
83
+ DROP PROCEDURE IF EXISTS __ensure_unique_index;