@kaikybrofc/omnizap-system 2.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +534 -0
- package/LICENSE +21 -0
- package/README.md +431 -0
- package/RELEASE-v2.1.2.md +83 -0
- package/app/config/adminIdentity.js +87 -0
- package/app/config/baileysConfig.js +693 -0
- package/app/config/groupUtils.js +388 -0
- package/app/connection/socketController.js +992 -0
- package/app/controllers/messageController.js +354 -0
- package/app/modules/adminModule/groupCommandHandlers.js +1294 -0
- package/app/modules/adminModule/groupEventHandlers.js +355 -0
- package/app/modules/aiModule/catCommand.js +1006 -0
- package/app/modules/broadcastModule/noticeCommand.js +416 -0
- package/app/modules/gameModule/diceCommand.js +67 -0
- package/app/modules/menuModule/common.js +311 -0
- package/app/modules/menuModule/menus.js +59 -0
- package/app/modules/playModule/playCommand.js +1615 -0
- package/app/modules/quoteModule/quoteCommand.js +851 -0
- package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +786 -0
- package/app/modules/rpgPokemonModule/rpgBattleService.js +2082 -0
- package/app/modules/rpgPokemonModule/rpgBattleService.test.js +760 -0
- package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
- package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +172 -0
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
- package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
- package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
- package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1859 -0
- package/app/modules/rpgPokemonModule/rpgPokemonService.js +6738 -0
- package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
- package/app/modules/statsModule/globalRankingCommand.js +65 -0
- package/app/modules/statsModule/noMessageCommand.js +288 -0
- package/app/modules/statsModule/rankingCommand.js +60 -0
- package/app/modules/statsModule/rankingCommon.js +889 -0
- package/app/modules/stickerModule/addStickerMetadata.js +239 -0
- package/app/modules/stickerModule/convertToWebp.js +390 -0
- package/app/modules/stickerModule/stickerCommand.js +454 -0
- package/app/modules/stickerModule/stickerConvertCommand.js +156 -0
- package/app/modules/stickerModule/stickerTextCommand.js +657 -0
- package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
- package/app/modules/stickerPackModule/autoPackCollectorService.js +284 -0
- package/app/modules/stickerPackModule/semanticReclassificationEngine.js +466 -0
- package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +88 -0
- package/app/modules/stickerPackModule/semanticThemeClusterService.js +571 -0
- package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +449 -0
- package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
- package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +180 -0
- package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +4078 -0
- package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +598 -0
- package/app/modules/stickerPackModule/stickerClassificationService.js +588 -0
- package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +102 -0
- package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +7506 -0
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1095 -0
- package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +108 -0
- package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
- package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +110 -0
- package/app/modules/stickerPackModule/stickerPackItemRepository.js +440 -0
- package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +337 -0
- package/app/modules/stickerPackModule/stickerPackMessageService.js +296 -0
- package/app/modules/stickerPackModule/stickerPackRepository.js +442 -0
- package/app/modules/stickerPackModule/stickerPackService.js +788 -0
- package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +51 -0
- package/app/modules/stickerPackModule/stickerPackUtils.js +97 -0
- package/app/modules/stickerPackModule/stickerStorageService.js +507 -0
- package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +233 -0
- package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +205 -0
- package/app/modules/systemMetricsModule/pingCommand.js +421 -0
- package/app/modules/tiktokModule/tiktokCommand.js +798 -0
- package/app/modules/userModule/userCommand.js +1217 -0
- package/app/modules/waifuPicsModule/waifuPicsCommand.js +177 -0
- package/app/observability/metrics.js +734 -0
- package/app/services/captchaService.js +492 -0
- package/app/services/dbWriteQueue.js +572 -0
- package/app/services/groupMetadataService.js +279 -0
- package/app/services/lidMapService.js +663 -0
- package/app/services/messagePersistenceService.js +56 -0
- package/app/services/newsBroadcastService.js +351 -0
- package/app/services/pokeApiService.js +398 -0
- package/app/services/queueUtils.js +57 -0
- package/app/services/socketState.js +7 -0
- package/app/store/aiPromptStore.js +38 -0
- package/app/store/groupConfigStore.js +58 -0
- package/app/store/premiumUserStore.js +36 -0
- package/app/utils/antiLink/antiLinkModule.js +804 -0
- package/app/utils/http/getImageBufferModule.js +18 -0
- package/app/utils/json/jsonSanitizer.js +113 -0
- package/app/utils/json/jsonSanitizer.test.js +40 -0
- package/app/utils/logger/loggerModule.js +262 -0
- package/app/utils/systemMetrics/systemMetricsModule.js +91 -0
- package/database/index.js +2052 -0
- package/database/init.js +516 -0
- package/database/migrations/20260203_0001_sticker_packs.sql +54 -0
- package/database/migrations/20260210_0003_rpg_pokemon.sql +58 -0
- package/database/migrations/20260210_0004_rpg_shiny_biome.sql +9 -0
- package/database/migrations/20260210_0005_rpg_missions.sql +14 -0
- package/database/migrations/20260210_0006_rpg_world_pokedex_traits.sql +27 -0
- package/database/migrations/20260210_0007_rpg_raid_pvp.sql +56 -0
- package/database/migrations/20260210_0008_rpg_social_system.sql +195 -0
- package/database/migrations/20260211_0009_rpg_social_xp.sql +36 -0
- package/database/migrations/20260222_0010_remove_message_xp.sql +2 -0
- package/database/migrations/20260226_0011_sticker_asset_classification.sql +17 -0
- package/database/migrations/20260226_0012_sticker_pack_engagement.sql +16 -0
- package/database/migrations/20260226_0013_sticker_marketplace_intelligence.sql +19 -0
- package/database/migrations/20260226_0014_sticker_pack_publish_flow.sql +30 -0
- package/database/migrations/20260226_0014_sticker_worker_queues.sql +42 -0
- package/database/migrations/20260226_0015_sticker_auto_pack_curation_integrity.sql +18 -0
- package/database/migrations/20260226_0016_sticker_web_google_auth_persistence.sql +34 -0
- package/database/migrations/20260226_0017_sticker_web_admin_ban.sql +22 -0
- package/database/migrations/20260226_0018_sticker_web_admin_moderator.sql +18 -0
- package/database/migrations/20260227_0019_sticker_classification_v2_signals.sql +12 -0
- package/database/migrations/20260227_0020_semantic_theme_clusters.sql +35 -0
- package/docker-compose.yml +103 -0
- package/ecosystem.prod.config.cjs +35 -0
- package/eslint.config.js +61 -0
- package/index.js +437 -0
- package/ml/clip_classifier/Dockerfile +16 -0
- package/ml/clip_classifier/README.md +120 -0
- package/ml/clip_classifier/adaptive_scoring.py +40 -0
- package/ml/clip_classifier/classifier.py +654 -0
- package/ml/clip_classifier/embedding_store.py +481 -0
- package/ml/clip_classifier/env_loader.py +15 -0
- package/ml/clip_classifier/llm_label_expander.py +144 -0
- package/ml/clip_classifier/main.py +213 -0
- package/ml/clip_classifier/requirements.txt +10 -0
- package/ml/clip_classifier/similarity_engine.py +74 -0
- package/observability/alert-rules.yml +60 -0
- package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
- package/observability/grafana/dashboards/omnizap-overview.json +170 -0
- package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
- package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
- package/observability/loki-config.yml +38 -0
- package/observability/mysql-exporter.cnf +5 -0
- package/observability/mysql-setup.sql +46 -0
- package/observability/prometheus.yml +32 -0
- package/observability/promtail-config.yml +84 -0
- package/package.json +109 -0
- package/public/api-docs/index.html +144 -0
- package/public/css/github-project-panel.css +297 -0
- package/public/css/stickers-admin.css +1272 -0
- package/public/css/styles.css +671 -0
- package/public/index.html +1311 -0
- package/public/js/apps/apiDocsApp.js +310 -0
- package/public/js/apps/createPackApp.js +2069 -0
- package/public/js/apps/homeApp.js +396 -0
- package/public/js/apps/stickersAdminApp.js +1744 -0
- package/public/js/apps/stickersApp.js +4830 -0
- package/public/js/catalog.js +1019 -0
- package/public/js/github-panel/components/CommitList.js +34 -0
- package/public/js/github-panel/components/ErrorState.js +16 -0
- package/public/js/github-panel/components/GithubProjectPanel.js +106 -0
- package/public/js/github-panel/components/ReleaseList.js +38 -0
- package/public/js/github-panel/components/SkeletonPanel.js +22 -0
- package/public/js/github-panel/components/StatCard.js +15 -0
- package/public/js/github-panel/index.js +15 -0
- package/public/js/github-panel/useGithubRepoData.js +154 -0
- package/public/js/github-panel/vendor/react.js +11 -0
- package/public/js/runtime/react-runtime.js +19 -0
- package/public/licenca/index.html +106 -0
- package/public/stickers/admin/index.html +23 -0
- package/public/stickers/create/index.html +47 -0
- package/public/stickers/index.html +48 -0
- package/public/termos-de-uso/index.html +125 -0
- package/scripts/cache-bust.mjs +107 -0
- package/scripts/deploy.sh +458 -0
- package/scripts/github-deploy-notify.mjs +174 -0
- package/scripts/release.sh +129 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import logger from '../utils/logger/loggerModule.js';
|
|
2
|
+
import { getJidUser } from '../config/baileysConfig.js';
|
|
3
|
+
import { isUserAdmin, updateGroupParticipants } from '../config/groupUtils.js';
|
|
4
|
+
import { extractUserIdInfo, resolveUserId, resolveUserIdCached } from './lidMapService.js';
|
|
5
|
+
import { sendAndStore } from './messagePersistenceService.js';
|
|
6
|
+
import { getActiveSocket } from './socketState.js';
|
|
7
|
+
|
|
8
|
+
export const CAPTCHA_TIMEOUT_MS = 5 * 60 * 1000;
|
|
9
|
+
export const CAPTCHA_TIMEOUT_MINUTES = 5;
|
|
10
|
+
const CAPTCHA_OK_EMOJI = process.env.CAPTCHA_OK_EMOJI || '✅';
|
|
11
|
+
|
|
12
|
+
const pendingCaptchas = new Map();
|
|
13
|
+
const captchaMessageState = new Map();
|
|
14
|
+
const NON_HUMAN_CAPTCHA_MESSAGE_TEXTS = new Set([
|
|
15
|
+
'Mensagem vazia',
|
|
16
|
+
'Tipo de mensagem não suportado ou sem conteúdo.',
|
|
17
|
+
'[Histórico de mensagens]',
|
|
18
|
+
'[Aviso de histórico de mensagens]',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const buildMessageStateKey = (groupId, messageId) => `${groupId}:${messageId}`;
|
|
22
|
+
const normalizeMessageText = (messageText) => (typeof messageText === 'string' ? messageText.trim() : '');
|
|
23
|
+
const hasMessageStubType = (messageInfo) => messageInfo?.messageStubType !== undefined && messageInfo?.messageStubType !== null;
|
|
24
|
+
|
|
25
|
+
const resolveMessagePayload = (messageInfo) => {
|
|
26
|
+
const payload = messageInfo?.message;
|
|
27
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
28
|
+
|
|
29
|
+
if (payload.deviceSentMessage?.message && typeof payload.deviceSentMessage.message === 'object') {
|
|
30
|
+
return payload.deviceSentMessage.message;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return payload;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const isHumanCaptchaMessage = ({ messageInfo, extractedText }) => {
|
|
37
|
+
const normalizedText = normalizeMessageText(extractedText);
|
|
38
|
+
if (!normalizedText || NON_HUMAN_CAPTCHA_MESSAGE_TEXTS.has(normalizedText)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!messageInfo || typeof messageInfo !== 'object' || hasMessageStubType(messageInfo)) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const payload = resolveMessagePayload(messageInfo);
|
|
47
|
+
if (!payload) return false;
|
|
48
|
+
|
|
49
|
+
if (payload.protocolMessage || payload.messageHistoryBundle || payload.messageHistoryNotice || payload.fastRatchetKeySenderKeyDistributionMessage) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return true;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const toNonEmptyString = (value) => {
|
|
57
|
+
if (typeof value !== 'string') return null;
|
|
58
|
+
const trimmed = value.trim();
|
|
59
|
+
return trimmed || null;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const appendCandidate = (set, value) => {
|
|
63
|
+
const normalized = toNonEmptyString(value);
|
|
64
|
+
if (normalized) {
|
|
65
|
+
set.add(normalized);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const buildUserIdCandidates = (...sources) => {
|
|
70
|
+
const candidateIds = new Set();
|
|
71
|
+
|
|
72
|
+
for (const source of sources) {
|
|
73
|
+
if (!source) continue;
|
|
74
|
+
const info = extractUserIdInfo(source);
|
|
75
|
+
appendCandidate(candidateIds, resolveUserIdCached(info));
|
|
76
|
+
appendCandidate(candidateIds, info.jid);
|
|
77
|
+
appendCandidate(candidateIds, info.lid);
|
|
78
|
+
appendCandidate(candidateIds, info.participantAlt);
|
|
79
|
+
appendCandidate(candidateIds, info.raw);
|
|
80
|
+
|
|
81
|
+
if (typeof source === 'object') {
|
|
82
|
+
appendCandidate(candidateIds, source.id);
|
|
83
|
+
appendCandidate(candidateIds, source.jid);
|
|
84
|
+
appendCandidate(candidateIds, source.lid);
|
|
85
|
+
appendCandidate(candidateIds, source.participant);
|
|
86
|
+
appendCandidate(candidateIds, source.participantAlt);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return Array.from(candidateIds);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const ensureGroupMap = (groupId) => {
|
|
94
|
+
if (!pendingCaptchas.has(groupId)) {
|
|
95
|
+
pendingCaptchas.set(groupId, new Map());
|
|
96
|
+
}
|
|
97
|
+
return pendingCaptchas.get(groupId);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const cleanupGroupIfEmpty = (groupId, groupMap) => {
|
|
101
|
+
if (!groupMap || groupMap.size > 0) return;
|
|
102
|
+
pendingCaptchas.delete(groupId);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const cleanupMessageStateForEntry = (entry) => {
|
|
106
|
+
if (!entry?.messageStateKey) return null;
|
|
107
|
+
const state = captchaMessageState.get(entry.messageStateKey);
|
|
108
|
+
if (!state) return null;
|
|
109
|
+
state.pendingCount = Math.max(0, (state.pendingCount || 0) - 1);
|
|
110
|
+
if (state.pendingCount <= 0) {
|
|
111
|
+
captchaMessageState.delete(entry.messageStateKey);
|
|
112
|
+
}
|
|
113
|
+
return state;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const removeEntryAliasesFromGroup = (groupMap, entry) => {
|
|
117
|
+
if (!groupMap || !entry) return;
|
|
118
|
+
for (const [key, mappedEntry] of groupMap.entries()) {
|
|
119
|
+
if (mappedEntry === entry) {
|
|
120
|
+
groupMap.delete(key);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const findPendingEntry = (groupId, ...identitySources) => {
|
|
126
|
+
const groupMap = pendingCaptchas.get(groupId);
|
|
127
|
+
if (!groupMap) return null;
|
|
128
|
+
|
|
129
|
+
const candidates = buildUserIdCandidates(...identitySources);
|
|
130
|
+
for (const candidate of candidates) {
|
|
131
|
+
const entry = groupMap.get(candidate);
|
|
132
|
+
if (entry) {
|
|
133
|
+
return { groupMap, entry, lookupUserId: candidate };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return null;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const findPendingEntryAsync = async (groupId, ...identitySources) => {
|
|
141
|
+
const directMatch = findPendingEntry(groupId, ...identitySources);
|
|
142
|
+
if (directMatch) return directMatch;
|
|
143
|
+
|
|
144
|
+
const groupMap = pendingCaptchas.get(groupId);
|
|
145
|
+
if (!groupMap) return null;
|
|
146
|
+
|
|
147
|
+
const asyncCandidates = new Set();
|
|
148
|
+
|
|
149
|
+
for (const source of identitySources) {
|
|
150
|
+
if (!source) continue;
|
|
151
|
+
const info = extractUserIdInfo(source);
|
|
152
|
+
const hasLidLikeIdentity = Boolean(info.lid || (typeof info.raw === 'string' && info.raw.includes('@lid')));
|
|
153
|
+
if (!hasLidLikeIdentity) continue;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const resolved = await resolveUserId(info);
|
|
157
|
+
appendCandidate(asyncCandidates, resolved);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
logger.warn('Falha ao resolver ID canônico no captcha via lid_map.', {
|
|
160
|
+
groupId,
|
|
161
|
+
errorMessage: error.message,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const candidate of asyncCandidates) {
|
|
167
|
+
const entry = groupMap.get(candidate);
|
|
168
|
+
if (entry) {
|
|
169
|
+
// guarda alias resolvido para próximos eventos do mesmo usuário
|
|
170
|
+
groupMap.set(candidate, entry);
|
|
171
|
+
return { groupMap, entry, lookupUserId: candidate };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return null;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const clearEntry = (groupId, userId, reason) => {
|
|
179
|
+
const groupMap = pendingCaptchas.get(groupId);
|
|
180
|
+
if (!groupMap) return null;
|
|
181
|
+
const entry = groupMap.get(userId);
|
|
182
|
+
if (!entry) return null;
|
|
183
|
+
|
|
184
|
+
if (entry.timeoutId) {
|
|
185
|
+
clearTimeout(entry.timeoutId);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
removeEntryAliasesFromGroup(groupMap, entry);
|
|
189
|
+
cleanupGroupIfEmpty(groupId, groupMap);
|
|
190
|
+
const messageState = cleanupMessageStateForEntry(entry);
|
|
191
|
+
|
|
192
|
+
logger.debug('Captcha resolvido/removido.', {
|
|
193
|
+
action: 'captcha_clear',
|
|
194
|
+
groupId,
|
|
195
|
+
userId: entry.userId || userId,
|
|
196
|
+
lookupUserId: userId,
|
|
197
|
+
reason,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return { entry, messageState };
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const handleCaptchaTimeout = async (groupId, userId) => {
|
|
204
|
+
const groupMap = pendingCaptchas.get(groupId);
|
|
205
|
+
const entry = groupMap?.get(userId);
|
|
206
|
+
if (!entry) return;
|
|
207
|
+
|
|
208
|
+
if (Date.now() < entry.expiresAt) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const cleared = clearEntry(groupId, userId, 'timeout');
|
|
213
|
+
if (!cleared) return;
|
|
214
|
+
const resolvedEntry = cleared.entry;
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const sock = getActiveSocket();
|
|
218
|
+
if (!sock) {
|
|
219
|
+
logger.warn('Socket ativo indisponível para aplicar timeout de captcha.', {
|
|
220
|
+
groupId,
|
|
221
|
+
userId,
|
|
222
|
+
});
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const removalId = resolvedEntry.rawId || userId;
|
|
227
|
+
const isAdmin = await isUserAdmin(groupId, removalId);
|
|
228
|
+
if (isAdmin) {
|
|
229
|
+
logger.info('Captcha expirado, mas usuário é admin. Nenhuma ação tomada.', {
|
|
230
|
+
groupId,
|
|
231
|
+
userId: removalId,
|
|
232
|
+
});
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
await updateGroupParticipants(sock, groupId, [removalId], 'remove');
|
|
237
|
+
|
|
238
|
+
const user = getJidUser(removalId);
|
|
239
|
+
await sendAndStore(sock, groupId, {
|
|
240
|
+
text: `⏳ @${user || 'usuario'} foi removido por não completar a verificação em ${CAPTCHA_TIMEOUT_MINUTES} minutos.`,
|
|
241
|
+
mentions: [removalId],
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
logger.info('Usuário removido por falha no captcha.', {
|
|
245
|
+
action: 'captcha_remove',
|
|
246
|
+
groupId,
|
|
247
|
+
userId: removalId,
|
|
248
|
+
});
|
|
249
|
+
} catch (error) {
|
|
250
|
+
logger.error('Erro ao aplicar timeout do captcha.', {
|
|
251
|
+
groupId,
|
|
252
|
+
userId,
|
|
253
|
+
errorMessage: error.message,
|
|
254
|
+
stack: error.stack,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const sendCaptchaApprovalReaction = async ({ groupId, messageKey, userId }) => {
|
|
260
|
+
if (!groupId || !messageKey || !CAPTCHA_OK_EMOJI) return;
|
|
261
|
+
const sock = getActiveSocket();
|
|
262
|
+
if (!sock) {
|
|
263
|
+
logger.warn('Socket ativo indisponível para enviar reação de captcha.', {
|
|
264
|
+
groupId,
|
|
265
|
+
userId,
|
|
266
|
+
});
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
await sendAndStore(sock, groupId, {
|
|
272
|
+
react: {
|
|
273
|
+
text: CAPTCHA_OK_EMOJI,
|
|
274
|
+
key: messageKey,
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
} catch (error) {
|
|
278
|
+
logger.warn('Falha ao enviar reação de captcha.', {
|
|
279
|
+
groupId,
|
|
280
|
+
userId,
|
|
281
|
+
errorMessage: error.message,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const sendCaptchaApprovalEdit = async ({ groupId, entry, messageState }) => {
|
|
287
|
+
if (!groupId || !entry?.messageKey) return;
|
|
288
|
+
|
|
289
|
+
const mentionId = entry.rawId || entry.userId;
|
|
290
|
+
const userLabel = getJidUser(mentionId) || 'usuario';
|
|
291
|
+
const approvalLine = `✅ @${userLabel} passou na verificação.`;
|
|
292
|
+
const baseText = normalizeMessageText(messageState?.text);
|
|
293
|
+
const updatedText = baseText.includes(approvalLine) ? baseText : `${baseText}${baseText ? '\n' : ''}${approvalLine}`;
|
|
294
|
+
|
|
295
|
+
const mentionsSet = new Set(Array.isArray(messageState?.mentions) ? messageState.mentions : []);
|
|
296
|
+
if (mentionId) {
|
|
297
|
+
mentionsSet.add(mentionId);
|
|
298
|
+
}
|
|
299
|
+
const mentions = Array.from(mentionsSet);
|
|
300
|
+
|
|
301
|
+
const sock = getActiveSocket();
|
|
302
|
+
if (!sock) {
|
|
303
|
+
logger.warn('Socket ativo indisponível para editar mensagem de captcha.', {
|
|
304
|
+
groupId,
|
|
305
|
+
userId: mentionId,
|
|
306
|
+
});
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
await sendAndStore(sock, groupId, {
|
|
312
|
+
text: updatedText,
|
|
313
|
+
mentions,
|
|
314
|
+
edit: entry.messageKey,
|
|
315
|
+
});
|
|
316
|
+
if (messageState) {
|
|
317
|
+
messageState.text = updatedText;
|
|
318
|
+
messageState.mentions = mentions;
|
|
319
|
+
}
|
|
320
|
+
} catch (error) {
|
|
321
|
+
logger.warn('Falha ao editar mensagem de captcha.', {
|
|
322
|
+
groupId,
|
|
323
|
+
userId: mentionId,
|
|
324
|
+
errorMessage: error.message,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const sendCaptchaApprovalNotice = async ({ groupId, entry, method }) => {
|
|
330
|
+
if (!groupId) return;
|
|
331
|
+
|
|
332
|
+
const mentionId = entry?.rawId || entry?.userId || null;
|
|
333
|
+
const userLabel = getJidUser(mentionId) || 'usuario';
|
|
334
|
+
const methodLabel = method === 'reaction' ? 'reação' : 'mensagem';
|
|
335
|
+
|
|
336
|
+
const sock = getActiveSocket();
|
|
337
|
+
if (!sock) {
|
|
338
|
+
logger.warn('Socket ativo indisponível para enviar aviso de aprovação no captcha.', {
|
|
339
|
+
groupId,
|
|
340
|
+
userId: mentionId,
|
|
341
|
+
method,
|
|
342
|
+
});
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
await sendAndStore(sock, groupId, {
|
|
348
|
+
text: `✅ @${userLabel} completou a verificação por ${methodLabel} e foi aprovado(a).`,
|
|
349
|
+
mentions: mentionId ? [mentionId] : [],
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
logger.info('Usuário aprovado no captcha.', {
|
|
353
|
+
action: 'captcha_approved',
|
|
354
|
+
groupId,
|
|
355
|
+
userId: mentionId,
|
|
356
|
+
method,
|
|
357
|
+
});
|
|
358
|
+
} catch (error) {
|
|
359
|
+
logger.warn('Falha ao enviar aviso de aprovação de captcha.', {
|
|
360
|
+
groupId,
|
|
361
|
+
userId: mentionId,
|
|
362
|
+
method,
|
|
363
|
+
errorMessage: error.message,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
export const registerCaptchaChallenge = ({ groupId, participantJid, messageKey, messageText, messageMentions }) => {
|
|
369
|
+
if (!groupId) return;
|
|
370
|
+
const userIdCandidates = buildUserIdCandidates(participantJid);
|
|
371
|
+
const userId = userIdCandidates[0] || null;
|
|
372
|
+
if (!userId) return;
|
|
373
|
+
const rawId = toNonEmptyString(participantJid) || userId;
|
|
374
|
+
|
|
375
|
+
const groupMap = ensureGroupMap(groupId);
|
|
376
|
+
const existingMatch = findPendingEntry(groupId, participantJid, userId);
|
|
377
|
+
if (existingMatch?.entry?.timeoutId) {
|
|
378
|
+
clearTimeout(existingMatch.entry.timeoutId);
|
|
379
|
+
}
|
|
380
|
+
if (existingMatch?.entry) {
|
|
381
|
+
removeEntryAliasesFromGroup(groupMap, existingMatch.entry);
|
|
382
|
+
cleanupMessageStateForEntry(existingMatch.entry);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const expiresAt = Date.now() + CAPTCHA_TIMEOUT_MS;
|
|
386
|
+
const messageId = messageKey?.id || null;
|
|
387
|
+
const normalizedText = normalizeMessageText(messageText);
|
|
388
|
+
const messageStateKey = messageId && normalizedText ? buildMessageStateKey(groupId, messageId) : null;
|
|
389
|
+
|
|
390
|
+
if (messageStateKey) {
|
|
391
|
+
const mentions = Array.isArray(messageMentions) ? Array.from(new Set(messageMentions.filter(Boolean))) : [];
|
|
392
|
+
const existingState = captchaMessageState.get(messageStateKey);
|
|
393
|
+
if (existingState) {
|
|
394
|
+
existingState.pendingCount = (existingState.pendingCount || 0) + 1;
|
|
395
|
+
if (!existingState.text) existingState.text = normalizedText;
|
|
396
|
+
if ((!existingState.mentions || existingState.mentions.length === 0) && mentions.length > 0) {
|
|
397
|
+
existingState.mentions = mentions;
|
|
398
|
+
}
|
|
399
|
+
} else {
|
|
400
|
+
captchaMessageState.set(messageStateKey, {
|
|
401
|
+
text: normalizedText,
|
|
402
|
+
mentions,
|
|
403
|
+
pendingCount: 1,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const timeoutId = setTimeout(() => {
|
|
409
|
+
handleCaptchaTimeout(groupId, userId);
|
|
410
|
+
}, CAPTCHA_TIMEOUT_MS);
|
|
411
|
+
|
|
412
|
+
const entry = {
|
|
413
|
+
userId,
|
|
414
|
+
rawId,
|
|
415
|
+
messageId,
|
|
416
|
+
messageKey: messageKey || null,
|
|
417
|
+
messageStateKey,
|
|
418
|
+
expiresAt,
|
|
419
|
+
timeoutId,
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const aliases = Array.from(new Set([userId, rawId, ...userIdCandidates].filter(Boolean)));
|
|
423
|
+
for (const alias of aliases) {
|
|
424
|
+
groupMap.set(alias, entry);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
logger.debug('Captcha iniciado para usuário.', {
|
|
428
|
+
action: 'captcha_start',
|
|
429
|
+
groupId,
|
|
430
|
+
userId,
|
|
431
|
+
expiresAt,
|
|
432
|
+
messageId,
|
|
433
|
+
});
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
export const clearCaptchaForUser = (groupId, participantJid, reason = 'manual') => {
|
|
437
|
+
const match = findPendingEntry(groupId, participantJid);
|
|
438
|
+
if (!match) return false;
|
|
439
|
+
const cleared = clearEntry(groupId, match.lookupUserId, reason);
|
|
440
|
+
return Boolean(cleared);
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
export const clearCaptchasForGroup = (groupId, reason = 'manual') => {
|
|
444
|
+
const groupMap = pendingCaptchas.get(groupId);
|
|
445
|
+
if (!groupMap) return;
|
|
446
|
+
|
|
447
|
+
const uniqueEntries = new Set(groupMap.values());
|
|
448
|
+
for (const entry of uniqueEntries) {
|
|
449
|
+
if (entry.timeoutId) clearTimeout(entry.timeoutId);
|
|
450
|
+
cleanupMessageStateForEntry(entry);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
pendingCaptchas.delete(groupId);
|
|
454
|
+
|
|
455
|
+
logger.debug('Captchas do grupo limpos.', {
|
|
456
|
+
action: 'captcha_clear_group',
|
|
457
|
+
groupId,
|
|
458
|
+
reason,
|
|
459
|
+
});
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
export const resolveCaptchaByMessage = async ({ groupId, senderJid, senderIdentity, messageKey, messageInfo, extractedText }) => {
|
|
463
|
+
if (!groupId) return false;
|
|
464
|
+
if (!isHumanCaptchaMessage({ messageInfo, extractedText })) return false;
|
|
465
|
+
const match = await findPendingEntryAsync(groupId, senderIdentity, senderJid);
|
|
466
|
+
if (!match) return false;
|
|
467
|
+
|
|
468
|
+
const cleared = clearEntry(groupId, match.lookupUserId, 'message');
|
|
469
|
+
if (!cleared) return false;
|
|
470
|
+
|
|
471
|
+
sendCaptchaApprovalReaction({ groupId, messageKey, userId: cleared.entry.userId || match.lookupUserId });
|
|
472
|
+
sendCaptchaApprovalNotice({ groupId, entry: cleared.entry, method: 'message' });
|
|
473
|
+
return true;
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
export const resolveCaptchaByReaction = async ({ groupId, senderJid, senderIdentity, reactedMessageId, reactionText }) => {
|
|
477
|
+
if (!groupId) return false;
|
|
478
|
+
if (!normalizeMessageText(reactionText)) return false;
|
|
479
|
+
const match = await findPendingEntryAsync(groupId, senderIdentity, senderJid);
|
|
480
|
+
if (!match?.entry) return false;
|
|
481
|
+
|
|
482
|
+
if (match.entry.messageId && match.entry.messageId !== reactedMessageId) {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const cleared = clearEntry(groupId, match.lookupUserId, 'reaction');
|
|
487
|
+
if (!cleared) return false;
|
|
488
|
+
|
|
489
|
+
sendCaptchaApprovalEdit({ groupId, entry: cleared.entry, messageState: cleared.messageState });
|
|
490
|
+
sendCaptchaApprovalNotice({ groupId, entry: cleared.entry, method: 'reaction' });
|
|
491
|
+
return true;
|
|
492
|
+
};
|