@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,354 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { createCanvas, loadImage } from 'canvas';
|
|
3
|
+
import logger from '../../utils/logger/loggerModule.js';
|
|
4
|
+
|
|
5
|
+
const WIDTH = 1600;
|
|
6
|
+
const HEIGHT = 1200;
|
|
7
|
+
const PANEL_RADIUS = 26;
|
|
8
|
+
const IMAGE_TIMEOUT_MS = Math.max(2_000, Number(process.env.RPG_PROFILE_CANVAS_TIMEOUT_MS) || 6_000);
|
|
9
|
+
const IMAGE_CACHE_TTL_MS = Math.max(2 * 60 * 1000, Number(process.env.RPG_PROFILE_CANVAS_CACHE_TTL_MS) || 10 * 60 * 1000);
|
|
10
|
+
const IMAGE_CACHE_LIMIT = Math.max(20, Number(process.env.RPG_PROFILE_CANVAS_CACHE_LIMIT) || 80);
|
|
11
|
+
|
|
12
|
+
const imageCache = globalThis.__omnizapProfileCanvasImageCache instanceof Map ? globalThis.__omnizapProfileCanvasImageCache : new Map();
|
|
13
|
+
globalThis.__omnizapProfileCanvasImageCache = imageCache;
|
|
14
|
+
|
|
15
|
+
const TYPE_COLORS = new Map([
|
|
16
|
+
['normal', '#a8a77a'],
|
|
17
|
+
['fire', '#ee8130'],
|
|
18
|
+
['water', '#6390f0'],
|
|
19
|
+
['electric', '#f7d02c'],
|
|
20
|
+
['grass', '#7ac74c'],
|
|
21
|
+
['ice', '#96d9d6'],
|
|
22
|
+
['fighting', '#c22e28'],
|
|
23
|
+
['poison', '#a33ea1'],
|
|
24
|
+
['ground', '#e2bf65'],
|
|
25
|
+
['flying', '#a98ff3'],
|
|
26
|
+
['psychic', '#f95587'],
|
|
27
|
+
['bug', '#a6b91a'],
|
|
28
|
+
['rock', '#b6a136'],
|
|
29
|
+
['ghost', '#735797'],
|
|
30
|
+
['dragon', '#6f35fc'],
|
|
31
|
+
['dark', '#705746'],
|
|
32
|
+
['steel', '#b7b7ce'],
|
|
33
|
+
['fairy', '#d685ad'],
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
37
|
+
|
|
38
|
+
const toInt = (value, fallback = 0) => {
|
|
39
|
+
const numeric = Number(value);
|
|
40
|
+
return Number.isFinite(numeric) ? Math.trunc(numeric) : fallback;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const toText = (value, fallback = 'N/D') => {
|
|
44
|
+
if (value === null || value === undefined) return fallback;
|
|
45
|
+
const text = String(value).trim();
|
|
46
|
+
return text || fallback;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const trimText = (value, max = 120) => {
|
|
50
|
+
const text = String(value || '')
|
|
51
|
+
.replace(/\s+/g, ' ')
|
|
52
|
+
.trim();
|
|
53
|
+
if (!text) return '';
|
|
54
|
+
if (text.length <= max) return text;
|
|
55
|
+
return `${text.slice(0, Math.max(16, max - 1)).trimEnd()}…`;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const drawRoundRect = (ctx, x, y, width, height, radius, fillStyle) => {
|
|
59
|
+
ctx.beginPath();
|
|
60
|
+
ctx.moveTo(x + radius, y);
|
|
61
|
+
ctx.lineTo(x + width - radius, y);
|
|
62
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
63
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
64
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
65
|
+
ctx.lineTo(x + radius, y + height);
|
|
66
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
67
|
+
ctx.lineTo(x, y + radius);
|
|
68
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
69
|
+
ctx.closePath();
|
|
70
|
+
if (fillStyle) {
|
|
71
|
+
ctx.fillStyle = fillStyle;
|
|
72
|
+
ctx.fill();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const cleanupImageCache = () => {
|
|
77
|
+
if (imageCache.size <= IMAGE_CACHE_LIMIT) return;
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
for (const [key, value] of imageCache.entries()) {
|
|
80
|
+
if (!value || value.expiresAt <= now) imageCache.delete(key);
|
|
81
|
+
}
|
|
82
|
+
while (imageCache.size > IMAGE_CACHE_LIMIT) {
|
|
83
|
+
const oldest = imageCache.keys().next().value;
|
|
84
|
+
imageCache.delete(oldest);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const resolveImage = async (url) => {
|
|
89
|
+
const normalized = String(url || '').trim();
|
|
90
|
+
if (!normalized) return null;
|
|
91
|
+
|
|
92
|
+
const cached = imageCache.get(normalized);
|
|
93
|
+
if (cached && cached.expiresAt > Date.now()) return cached.image;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const response = await axios.get(normalized, {
|
|
97
|
+
responseType: 'arraybuffer',
|
|
98
|
+
timeout: IMAGE_TIMEOUT_MS,
|
|
99
|
+
validateStatus: (status) => status >= 200 && status < 400,
|
|
100
|
+
});
|
|
101
|
+
const image = await loadImage(Buffer.from(response.data));
|
|
102
|
+
imageCache.set(normalized, {
|
|
103
|
+
image,
|
|
104
|
+
expiresAt: Date.now() + IMAGE_CACHE_TTL_MS,
|
|
105
|
+
});
|
|
106
|
+
cleanupImageCache();
|
|
107
|
+
return image;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
logger.debug('Falha ao carregar imagem para perfil canvas.', {
|
|
110
|
+
url: normalized,
|
|
111
|
+
error: error.message,
|
|
112
|
+
});
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const drawProgressBar = ({ ctx, x, y, width, height, progressPct = 0, color = '#22d3ee', background = 'rgba(15,23,42,0.75)' }) => {
|
|
118
|
+
drawRoundRect(ctx, x, y, width, height, Math.min(14, Math.round(height / 2)), background);
|
|
119
|
+
const ratio = clamp(Number(progressPct) / 100, 0, 1);
|
|
120
|
+
if (ratio <= 0) return;
|
|
121
|
+
drawRoundRect(ctx, x + 2, y + 2, Math.max(0, (width - 4) * ratio), Math.max(0, height - 4), Math.min(12, Math.round((height - 4) / 2)), color);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const sanitizeProfileText = (text) => {
|
|
125
|
+
const raw = String(text || '');
|
|
126
|
+
return raw
|
|
127
|
+
.split('\n')
|
|
128
|
+
.map((line) =>
|
|
129
|
+
String(line || '')
|
|
130
|
+
.replace(/\*/g, '')
|
|
131
|
+
.replace(/\s+$/g, ''),
|
|
132
|
+
)
|
|
133
|
+
.filter((line, index, lines) => line || (index > 0 && lines[index - 1]));
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const wrapLine = (ctx, line, maxWidth) => {
|
|
137
|
+
const text = String(line || '').trim();
|
|
138
|
+
if (!text) return [''];
|
|
139
|
+
|
|
140
|
+
const words = text.split(' ');
|
|
141
|
+
const wrapped = [];
|
|
142
|
+
let current = '';
|
|
143
|
+
|
|
144
|
+
for (const word of words) {
|
|
145
|
+
const candidate = current ? `${current} ${word}` : word;
|
|
146
|
+
if (ctx.measureText(candidate).width <= maxWidth) {
|
|
147
|
+
current = candidate;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (current) {
|
|
151
|
+
wrapped.push(current);
|
|
152
|
+
current = word;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let chunk = word;
|
|
157
|
+
while (chunk.length > 1 && ctx.measureText(chunk).width > maxWidth) {
|
|
158
|
+
const safeSize = Math.max(1, Math.floor((maxWidth / Math.max(1, ctx.measureText(chunk).width)) * chunk.length));
|
|
159
|
+
const part = chunk.slice(0, safeSize);
|
|
160
|
+
wrapped.push(`${part}…`);
|
|
161
|
+
chunk = chunk.slice(safeSize);
|
|
162
|
+
}
|
|
163
|
+
current = chunk;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (current) wrapped.push(current);
|
|
167
|
+
return wrapped.length ? wrapped : [''];
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const isHeaderLine = (line) => {
|
|
171
|
+
const value = String(line || '').trim();
|
|
172
|
+
if (!value) return false;
|
|
173
|
+
if (/^\d+\./.test(value)) return false;
|
|
174
|
+
if (value.startsWith('•')) return false;
|
|
175
|
+
return value.length <= 36;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export const renderProfileCanvasCard = async ({ trainerLabel = 'Treinador', generatedAtLabel = null, activePokemon = null, summary = {}, profileText = '' }) => {
|
|
179
|
+
const canvas = createCanvas(WIDTH, HEIGHT);
|
|
180
|
+
const ctx = canvas.getContext('2d');
|
|
181
|
+
|
|
182
|
+
const primaryType = String(activePokemon?.types?.[0] || '')
|
|
183
|
+
.trim()
|
|
184
|
+
.toLowerCase();
|
|
185
|
+
const accent = TYPE_COLORS.get(primaryType) || '#38bdf8';
|
|
186
|
+
|
|
187
|
+
const bgGradient = ctx.createLinearGradient(0, 0, WIDTH, HEIGHT);
|
|
188
|
+
bgGradient.addColorStop(0, '#0b1220');
|
|
189
|
+
bgGradient.addColorStop(0.55, '#111827');
|
|
190
|
+
bgGradient.addColorStop(1, '#1e293b');
|
|
191
|
+
ctx.fillStyle = bgGradient;
|
|
192
|
+
ctx.fillRect(0, 0, WIDTH, HEIGHT);
|
|
193
|
+
|
|
194
|
+
const glow = ctx.createRadialGradient(240, 240, 40, 240, 240, 560);
|
|
195
|
+
glow.addColorStop(0, `${accent}70`);
|
|
196
|
+
glow.addColorStop(1, 'rgba(2,6,23,0)');
|
|
197
|
+
ctx.fillStyle = glow;
|
|
198
|
+
ctx.fillRect(0, 0, WIDTH, HEIGHT);
|
|
199
|
+
|
|
200
|
+
const leftX = 40;
|
|
201
|
+
const leftY = 40;
|
|
202
|
+
const leftW = 470;
|
|
203
|
+
const leftH = HEIGHT - 80;
|
|
204
|
+
drawRoundRect(ctx, leftX, leftY, leftW, leftH, PANEL_RADIUS, 'rgba(2, 6, 23, 0.72)');
|
|
205
|
+
|
|
206
|
+
const rightX = leftX + leftW + 24;
|
|
207
|
+
const rightY = 40;
|
|
208
|
+
const rightW = WIDTH - rightX - 40;
|
|
209
|
+
const rightH = HEIGHT - 80;
|
|
210
|
+
drawRoundRect(ctx, rightX, rightY, rightW, rightH, PANEL_RADIUS, 'rgba(2, 6, 23, 0.65)');
|
|
211
|
+
|
|
212
|
+
ctx.fillStyle = '#e2e8f0';
|
|
213
|
+
ctx.font = '700 40px Sans';
|
|
214
|
+
ctx.fillText('PERFIL RPG', leftX + 30, leftY + 62);
|
|
215
|
+
|
|
216
|
+
ctx.fillStyle = '#94a3b8';
|
|
217
|
+
ctx.font = '500 22px Sans';
|
|
218
|
+
ctx.fillText(trimText(trainerLabel, 22), leftX + 30, leftY + 96);
|
|
219
|
+
|
|
220
|
+
const levelText = `Nivel ${toInt(summary?.level, 1)}`;
|
|
221
|
+
const goldText = `${toInt(summary?.gold, 0)} gold`;
|
|
222
|
+
ctx.fillStyle = '#f8fafc';
|
|
223
|
+
ctx.font = '700 28px Sans';
|
|
224
|
+
ctx.fillText(levelText, leftX + 30, leftY + 146);
|
|
225
|
+
ctx.fillText(goldText, leftX + 30, leftY + 184);
|
|
226
|
+
|
|
227
|
+
ctx.fillStyle = '#94a3b8';
|
|
228
|
+
ctx.font = '500 20px Sans';
|
|
229
|
+
const rankText = Number.isFinite(Number(summary?.pvpWeeklyRank)) ? `Rank PvP: #${toInt(summary?.pvpWeeklyRank, 0)}` : 'Rank PvP: sem rank';
|
|
230
|
+
const streakText = `Streak: ${toText(summary?.streakLabel, 'Sem historico')}`;
|
|
231
|
+
ctx.fillText(trimText(rankText, 34), leftX + 30, leftY + 220);
|
|
232
|
+
ctx.fillText(trimText(streakText, 34), leftX + 30, leftY + 248);
|
|
233
|
+
|
|
234
|
+
const xpProgressPct = clamp(toInt(summary?.xpProgressPct, 0), 0, 100);
|
|
235
|
+
drawProgressBar({
|
|
236
|
+
ctx,
|
|
237
|
+
x: leftX + 30,
|
|
238
|
+
y: leftY + 272,
|
|
239
|
+
width: leftW - 60,
|
|
240
|
+
height: 24,
|
|
241
|
+
progressPct: xpProgressPct,
|
|
242
|
+
color: accent,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
ctx.fillStyle = '#e2e8f0';
|
|
246
|
+
ctx.font = '600 18px Sans';
|
|
247
|
+
ctx.fillText(`XP: ${toInt(summary?.xp, 0)} (${xpProgressPct}%)`, leftX + 30, leftY + 325);
|
|
248
|
+
if (toInt(summary?.xpToNextLevel, 0) > 0) {
|
|
249
|
+
ctx.fillText(`Faltam ${toInt(summary?.xpToNextLevel, 0)} XP para o nivel ${toInt(summary?.nextLevel, toInt(summary?.level, 1) + 1)}`, leftX + 30, leftY + 350);
|
|
250
|
+
} else {
|
|
251
|
+
ctx.fillText('Nivel maximo alcancado', leftX + 30, leftY + 350);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const spriteX = leftX + 30;
|
|
255
|
+
const spriteY = leftY + 380;
|
|
256
|
+
const spriteW = leftW - 60;
|
|
257
|
+
const spriteH = 330;
|
|
258
|
+
drawRoundRect(ctx, spriteX, spriteY, spriteW, spriteH, 22, 'rgba(15, 23, 42, 0.8)');
|
|
259
|
+
|
|
260
|
+
const image = await resolveImage(activePokemon?.imageUrl || activePokemon?.sprite || null);
|
|
261
|
+
if (image) {
|
|
262
|
+
const size = Math.min(spriteW - 36, spriteH - 36);
|
|
263
|
+
const drawX = spriteX + (spriteW - size) / 2;
|
|
264
|
+
const drawY = spriteY + (spriteH - size) / 2;
|
|
265
|
+
ctx.drawImage(image, drawX, drawY, size, size);
|
|
266
|
+
} else {
|
|
267
|
+
ctx.fillStyle = 'rgba(148, 163, 184, 0.2)';
|
|
268
|
+
drawRoundRect(ctx, spriteX + 18, spriteY + 18, spriteW - 36, spriteH - 36, 18, 'rgba(148, 163, 184, 0.14)');
|
|
269
|
+
ctx.fillStyle = '#94a3b8';
|
|
270
|
+
ctx.font = '600 22px Sans';
|
|
271
|
+
ctx.textAlign = 'center';
|
|
272
|
+
ctx.fillText('Imagem indisponivel', spriteX + spriteW / 2, spriteY + spriteH / 2 + 8);
|
|
273
|
+
ctx.textAlign = 'start';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const pokemonName = trimText(activePokemon?.displayName || activePokemon?.name || 'Sem Pokemon ativo', 28);
|
|
277
|
+
ctx.fillStyle = '#f8fafc';
|
|
278
|
+
ctx.font = '700 26px Sans';
|
|
279
|
+
ctx.fillText(pokemonName, leftX + 30, leftY + 748);
|
|
280
|
+
|
|
281
|
+
const hpCurrent = Math.max(0, toInt(activePokemon?.currentHp, 0));
|
|
282
|
+
const hpMax = Math.max(1, toInt(activePokemon?.maxHp, 1));
|
|
283
|
+
const typeText = Array.isArray(activePokemon?.types) && activePokemon.types.length ? activePokemon.types.join(', ') : 'tipo indefinido';
|
|
284
|
+
ctx.fillStyle = '#94a3b8';
|
|
285
|
+
ctx.font = '500 20px Sans';
|
|
286
|
+
ctx.fillText(`Lv.${Math.max(1, toInt(activePokemon?.level, 1))} | HP ${hpCurrent}/${hpMax}`, leftX + 30, leftY + 782);
|
|
287
|
+
ctx.fillText(trimText(`Tipos: ${typeText}`, 36), leftX + 30, leftY + 812);
|
|
288
|
+
|
|
289
|
+
if (generatedAtLabel) {
|
|
290
|
+
ctx.fillStyle = '#64748b';
|
|
291
|
+
ctx.font = '500 17px Sans';
|
|
292
|
+
ctx.fillText(`Atualizado em ${generatedAtLabel}`, leftX + 30, leftY + leftH - 24);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
ctx.fillStyle = '#e2e8f0';
|
|
296
|
+
ctx.font = '700 34px Sans';
|
|
297
|
+
ctx.fillText('Informacoes do jogador', rightX + 28, rightY + 56);
|
|
298
|
+
|
|
299
|
+
ctx.fillStyle = '#94a3b8';
|
|
300
|
+
ctx.font = '500 18px Sans';
|
|
301
|
+
ctx.fillText('Resumo completo em duas colunas', rightX + 28, rightY + 84);
|
|
302
|
+
|
|
303
|
+
const textLines = sanitizeProfileText(profileText);
|
|
304
|
+
ctx.font = '500 20px Sans';
|
|
305
|
+
|
|
306
|
+
const contentTop = rightY + 122;
|
|
307
|
+
const contentBottom = rightY + rightH - 28;
|
|
308
|
+
const lineHeight = 24;
|
|
309
|
+
const colGap = 28;
|
|
310
|
+
const colWidth = Math.floor((rightW - 56 - colGap) / 2);
|
|
311
|
+
const colOneX = rightX + 28;
|
|
312
|
+
const colTwoX = colOneX + colWidth + colGap;
|
|
313
|
+
const maxLinesPerCol = Math.max(1, Math.floor((contentBottom - contentTop) / lineHeight));
|
|
314
|
+
const maxLines = maxLinesPerCol * 2;
|
|
315
|
+
|
|
316
|
+
const wrapped = [];
|
|
317
|
+
for (const line of textLines) {
|
|
318
|
+
const normalized = String(line || '');
|
|
319
|
+
if (!normalized.trim()) {
|
|
320
|
+
wrapped.push('');
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const segments = wrapLine(ctx, normalized, colWidth - 2);
|
|
324
|
+
segments.forEach((segment) => wrapped.push(segment));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const truncated = wrapped.length > maxLines;
|
|
328
|
+
const visible = wrapped.slice(0, maxLines);
|
|
329
|
+
if (truncated && visible.length) {
|
|
330
|
+
visible[visible.length - 1] = '... use /rpg perfil para ver texto completo';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
visible.forEach((line, index) => {
|
|
334
|
+
const colIndex = Math.floor(index / maxLinesPerCol);
|
|
335
|
+
const rowIndex = index % maxLinesPerCol;
|
|
336
|
+
const x = colIndex === 0 ? colOneX : colTwoX;
|
|
337
|
+
const y = contentTop + rowIndex * lineHeight;
|
|
338
|
+
|
|
339
|
+
if (!line) return;
|
|
340
|
+
|
|
341
|
+
if (isHeaderLine(line)) {
|
|
342
|
+
ctx.fillStyle = '#f8fafc';
|
|
343
|
+
ctx.font = '700 20px Sans';
|
|
344
|
+
ctx.fillText(trimText(line, 64), x, y);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
ctx.fillStyle = '#cbd5e1';
|
|
349
|
+
ctx.font = '500 19px Sans';
|
|
350
|
+
ctx.fillText(trimText(line, 94), x, y);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
return canvas.toBuffer('image/png', { compressionLevel: 4 });
|
|
354
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import logger from '../../utils/logger/loggerModule.js';
|
|
2
|
+
import { resolveBotJid } from '../../config/baileysConfig.js';
|
|
3
|
+
import { isWhatsAppUserId } from '../../services/lidMapService.js';
|
|
4
|
+
import { buildRankingMessage, getRankingReport, renderRankingImage } from './rankingCommon.js';
|
|
5
|
+
import { sendAndStore } from '../../services/messagePersistenceService.js';
|
|
6
|
+
|
|
7
|
+
const RANKING_LIMIT = 5;
|
|
8
|
+
const RENDER_TIMEOUT_MS = 15000;
|
|
9
|
+
|
|
10
|
+
const withTimeout = (promise, timeoutMs) =>
|
|
11
|
+
Promise.race([
|
|
12
|
+
promise,
|
|
13
|
+
new Promise((_, reject) => {
|
|
14
|
+
setTimeout(() => reject(new Error(`Tempo limite excedido (${timeoutMs}ms)`)), timeoutMs);
|
|
15
|
+
}),
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Handler do comando de ranking global.
|
|
20
|
+
* @param {object} params
|
|
21
|
+
* @param {object} params.sock
|
|
22
|
+
* @param {string} params.remoteJid
|
|
23
|
+
* @param {object} params.messageInfo
|
|
24
|
+
* @param {number|undefined} params.expirationMessage
|
|
25
|
+
* @param {boolean|undefined} params.isGroupMessage
|
|
26
|
+
* @returns {Promise<void>}
|
|
27
|
+
*/
|
|
28
|
+
export async function handleGlobalRankingCommand({ sock, remoteJid, messageInfo, expirationMessage, isGroupMessage: _isGroupMessage }) {
|
|
29
|
+
try {
|
|
30
|
+
const botJid = resolveBotJid(sock?.user?.id);
|
|
31
|
+
const report = await getRankingReport({
|
|
32
|
+
scope: 'global',
|
|
33
|
+
botJid,
|
|
34
|
+
limit: RANKING_LIMIT,
|
|
35
|
+
includeTopType: false,
|
|
36
|
+
enrichRows: false,
|
|
37
|
+
});
|
|
38
|
+
const text = buildRankingMessage({ scope: 'global', limit: RANKING_LIMIT, ...report });
|
|
39
|
+
const mentions = report.rows.map((row) => row.mention_id).filter((jid) => isWhatsAppUserId(jid));
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const imageBuffer = await withTimeout(
|
|
43
|
+
renderRankingImage({
|
|
44
|
+
sock,
|
|
45
|
+
remoteJid,
|
|
46
|
+
rows: report.rows,
|
|
47
|
+
totalMessages: report.totalMessages,
|
|
48
|
+
topType: report.topType,
|
|
49
|
+
scope: 'global',
|
|
50
|
+
limit: RANKING_LIMIT,
|
|
51
|
+
}),
|
|
52
|
+
RENDER_TIMEOUT_MS,
|
|
53
|
+
);
|
|
54
|
+
await sendAndStore(sock, remoteJid, { image: imageBuffer, caption: text, ...(mentions.length ? { mentions } : {}) }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
|
|
55
|
+
} catch (renderError) {
|
|
56
|
+
logger.warn('Falha/timeout ao renderizar imagem do ranking global; enviando somente texto.', {
|
|
57
|
+
error: renderError?.message,
|
|
58
|
+
});
|
|
59
|
+
await sendAndStore(sock, remoteJid, { text, ...(mentions.length ? { mentions } : {}) }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
logger.error('Erro ao gerar ranking global:', { error: error.message });
|
|
63
|
+
await sendAndStore(sock, remoteJid, { text: `Erro ao gerar ranking global: ${error.message}` }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { executeQuery } from '../../../database/index.js';
|
|
2
|
+
import logger from '../../utils/logger/loggerModule.js';
|
|
3
|
+
import { getGroupParticipants, isUserAdmin } from '../../config/groupUtils.js';
|
|
4
|
+
import { resolveBotJid } from '../../config/baileysConfig.js';
|
|
5
|
+
import {
|
|
6
|
+
primeLidCache,
|
|
7
|
+
resolveUserIdCached,
|
|
8
|
+
isLidUserId,
|
|
9
|
+
isWhatsAppUserId,
|
|
10
|
+
} from '../../services/lidMapService.js';
|
|
11
|
+
import { sendAndStore } from '../../services/messagePersistenceService.js';
|
|
12
|
+
|
|
13
|
+
const getParticipantJid = (participant) =>
|
|
14
|
+
participant?.id || participant?.jid || participant?.lid || null;
|
|
15
|
+
|
|
16
|
+
const MAX_MENTIONS_PER_MESSAGE = 80;
|
|
17
|
+
const BATCH_DELAY_MS = 400;
|
|
18
|
+
|
|
19
|
+
const parseMinMessages = (text = '') => {
|
|
20
|
+
const tokens = text.trim().split(/\s+/).filter(Boolean);
|
|
21
|
+
let min = null;
|
|
22
|
+
tokens.forEach((token) => {
|
|
23
|
+
const minMatch = /^min=(\d+)$/i.exec(token);
|
|
24
|
+
if (minMatch) {
|
|
25
|
+
min = Number(minMatch[1]);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (/^\d+$/.test(token) && min === null) {
|
|
29
|
+
min = Number(token);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
if (!Number.isFinite(min)) return 1;
|
|
33
|
+
return Math.max(0, min);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const parsePeriod = (text = '') => {
|
|
37
|
+
const tokens = text.trim().split(/\s+/).filter(Boolean);
|
|
38
|
+
let days = null;
|
|
39
|
+
let all = false;
|
|
40
|
+
tokens.forEach((token) => {
|
|
41
|
+
const lower = token.toLowerCase();
|
|
42
|
+
if (lower === 'all') all = true;
|
|
43
|
+
const match = /^(\d+)d$/i.exec(lower);
|
|
44
|
+
if (match) {
|
|
45
|
+
days = Number(match[1]);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
if (all || !days || days <= 0) {
|
|
49
|
+
return { sinceDate: null, label: 'histórico completo' };
|
|
50
|
+
}
|
|
51
|
+
const sinceDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
52
|
+
return { sinceDate, label: `últimos ${days} dias` };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const fetchMessageCounts = async (remoteJid, sinceDate) => {
|
|
56
|
+
const params = [remoteJid];
|
|
57
|
+
let sql = 'SELECT sender_id, COUNT(*) AS total FROM messages WHERE chat_id = ?';
|
|
58
|
+
if (sinceDate) {
|
|
59
|
+
sql += ' AND created_at >= ?';
|
|
60
|
+
params.push(sinceDate);
|
|
61
|
+
}
|
|
62
|
+
sql += ' GROUP BY sender_id';
|
|
63
|
+
return executeQuery(sql, params);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const normalizeParticipant = (participant, sock) => {
|
|
67
|
+
const rawId = getParticipantJid(participant);
|
|
68
|
+
const participantAlt = participant?.participantAlt || participant?.jid || participant?.id || null;
|
|
69
|
+
const canonical = resolveUserIdCached({
|
|
70
|
+
lid: rawId,
|
|
71
|
+
jid: rawId,
|
|
72
|
+
participantAlt,
|
|
73
|
+
});
|
|
74
|
+
const contact =
|
|
75
|
+
(canonical && sock?.contacts?.[canonical]) ||
|
|
76
|
+
(participantAlt && sock?.contacts?.[participantAlt]) ||
|
|
77
|
+
(rawId && sock?.contacts?.[rawId]) ||
|
|
78
|
+
null;
|
|
79
|
+
const displayName =
|
|
80
|
+
participant?.notify || participant?.name || contact?.notify || contact?.name || contact?.short;
|
|
81
|
+
return {
|
|
82
|
+
rawId,
|
|
83
|
+
participantAlt,
|
|
84
|
+
canonical: canonical || rawId || null,
|
|
85
|
+
displayName: displayName || null,
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const buildNoMessageText = ({
|
|
90
|
+
minMessages,
|
|
91
|
+
periodLabel,
|
|
92
|
+
totalParticipants,
|
|
93
|
+
totalListed,
|
|
94
|
+
batchIndex = 1,
|
|
95
|
+
batchTotal = 1,
|
|
96
|
+
batchSize = 0,
|
|
97
|
+
}) => {
|
|
98
|
+
const title =
|
|
99
|
+
minMessages <= 1 ? '🔇 *Membros sem mensagens no grupo*' : '🔇 *Membros abaixo do mínimo*';
|
|
100
|
+
const lines = [
|
|
101
|
+
title,
|
|
102
|
+
'',
|
|
103
|
+
`• Mínimo de mensagens: ${minMessages}`,
|
|
104
|
+
`• Período: ${periodLabel}`,
|
|
105
|
+
`• Participantes: ${totalParticipants}`,
|
|
106
|
+
`• Abaixo do mínimo: ${totalListed}`,
|
|
107
|
+
...(batchTotal > 1
|
|
108
|
+
? [`• Parte: ${batchIndex}/${batchTotal}`, `• Notificados nesta mensagem: ${batchSize}`]
|
|
109
|
+
: []),
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
if (!totalListed) {
|
|
113
|
+
lines.push('', '✅ Todos os membros atingiram o mínimo.');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return lines.join('\n');
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
120
|
+
|
|
121
|
+
const splitEntriesByMentions = (entries, maxMentions) => {
|
|
122
|
+
if (!maxMentions || maxMentions <= 0) return [entries];
|
|
123
|
+
const batches = [];
|
|
124
|
+
let current = [];
|
|
125
|
+
let mentionCount = 0;
|
|
126
|
+
|
|
127
|
+
entries.forEach((entry) => {
|
|
128
|
+
const needsMention = Boolean(entry.mentionJid);
|
|
129
|
+
if (current.length > 0 && needsMention && mentionCount + 1 > maxMentions) {
|
|
130
|
+
batches.push(current);
|
|
131
|
+
current = [];
|
|
132
|
+
mentionCount = 0;
|
|
133
|
+
}
|
|
134
|
+
current.push(entry);
|
|
135
|
+
if (needsMention) mentionCount += 1;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (current.length) batches.push(current);
|
|
139
|
+
return batches;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export async function handleNoMessageCommand({
|
|
143
|
+
sock,
|
|
144
|
+
remoteJid,
|
|
145
|
+
messageInfo,
|
|
146
|
+
expirationMessage,
|
|
147
|
+
isGroupMessage,
|
|
148
|
+
senderJid,
|
|
149
|
+
text,
|
|
150
|
+
}) {
|
|
151
|
+
if (!isGroupMessage) {
|
|
152
|
+
await sendAndStore(sock,
|
|
153
|
+
remoteJid,
|
|
154
|
+
{ text: 'Este comando so pode ser usado em grupos.' },
|
|
155
|
+
{ quoted: messageInfo, ephemeralExpiration: expirationMessage },
|
|
156
|
+
);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (!(await isUserAdmin(remoteJid, senderJid))) {
|
|
160
|
+
await sendAndStore(sock,
|
|
161
|
+
remoteJid,
|
|
162
|
+
{ text: 'Você não tem permissão para usar este comando.' },
|
|
163
|
+
{ quoted: messageInfo, ephemeralExpiration: expirationMessage },
|
|
164
|
+
);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const participants = await getGroupParticipants(remoteJid);
|
|
170
|
+
if (!participants || participants.length === 0) {
|
|
171
|
+
await sendAndStore(sock,
|
|
172
|
+
remoteJid,
|
|
173
|
+
{ text: 'Nao foi possivel obter os participantes do grupo.' },
|
|
174
|
+
{ quoted: messageInfo, ephemeralExpiration: expirationMessage },
|
|
175
|
+
);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const minMessages = parseMinMessages(text || '');
|
|
180
|
+
const { sinceDate, label: periodLabel } = parsePeriod(text || '');
|
|
181
|
+
const senderRows = await fetchMessageCounts(remoteJid, sinceDate);
|
|
182
|
+
const senderIds = senderRows.map((row) => row.sender_id).filter(Boolean);
|
|
183
|
+
|
|
184
|
+
const lidsToPrime = new Set();
|
|
185
|
+
senderIds.forEach((id) => {
|
|
186
|
+
if (isLidUserId(id)) lidsToPrime.add(id);
|
|
187
|
+
});
|
|
188
|
+
participants.forEach((participant) => {
|
|
189
|
+
const rawId = getParticipantJid(participant);
|
|
190
|
+
if (isLidUserId(rawId)) lidsToPrime.add(rawId);
|
|
191
|
+
});
|
|
192
|
+
if (lidsToPrime.size > 0) {
|
|
193
|
+
await primeLidCache(Array.from(lidsToPrime));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const countsByCanonical = new Map();
|
|
197
|
+
senderRows.forEach((row) => {
|
|
198
|
+
const rawId = row.sender_id;
|
|
199
|
+
if (!rawId) return;
|
|
200
|
+
const canonical = resolveUserIdCached({ lid: rawId, jid: rawId });
|
|
201
|
+
if (!canonical) return;
|
|
202
|
+
const total = Number(row.total || 0);
|
|
203
|
+
countsByCanonical.set(canonical, (countsByCanonical.get(canonical) || 0) + total);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const normalizedParticipants = new Map();
|
|
207
|
+
participants.forEach((participant) => {
|
|
208
|
+
const normalized = normalizeParticipant(participant, sock);
|
|
209
|
+
if (!normalized.canonical && !normalized.rawId) return;
|
|
210
|
+
const key = normalized.canonical || normalized.rawId;
|
|
211
|
+
if (!key) return;
|
|
212
|
+
if (!normalizedParticipants.has(key)) {
|
|
213
|
+
normalizedParticipants.set(key, normalized);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const botJid = resolveBotJid(sock?.user?.id);
|
|
218
|
+
const botCanonical = botJid ? resolveUserIdCached({ jid: botJid, lid: botJid }) : null;
|
|
219
|
+
|
|
220
|
+
const entries = [];
|
|
221
|
+
normalizedParticipants.forEach((participant) => {
|
|
222
|
+
const canonical = participant.canonical || participant.rawId;
|
|
223
|
+
if (!canonical) return;
|
|
224
|
+
if (botCanonical && canonical === botCanonical) return;
|
|
225
|
+
const total = countsByCanonical.get(canonical) || 0;
|
|
226
|
+
if (total >= minMessages) return;
|
|
227
|
+
const mentionJid = isWhatsAppUserId(canonical)
|
|
228
|
+
? canonical
|
|
229
|
+
: isWhatsAppUserId(participant.participantAlt)
|
|
230
|
+
? participant.participantAlt
|
|
231
|
+
: null;
|
|
232
|
+
entries.push({
|
|
233
|
+
canonical,
|
|
234
|
+
rawId: participant.rawId,
|
|
235
|
+
mentionJid,
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const totalParticipants = normalizedParticipants.size;
|
|
240
|
+
const totalListed = entries.length;
|
|
241
|
+
const batches = splitEntriesByMentions(entries, MAX_MENTIONS_PER_MESSAGE);
|
|
242
|
+
if (!batches.length) {
|
|
243
|
+
const responseText = buildNoMessageText({
|
|
244
|
+
minMessages,
|
|
245
|
+
periodLabel,
|
|
246
|
+
totalParticipants,
|
|
247
|
+
totalListed,
|
|
248
|
+
});
|
|
249
|
+
await sendAndStore(sock,
|
|
250
|
+
remoteJid,
|
|
251
|
+
{ text: responseText },
|
|
252
|
+
{ quoted: messageInfo, ephemeralExpiration: expirationMessage },
|
|
253
|
+
);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (let index = 0; index < batches.length; index += 1) {
|
|
258
|
+
const batch = batches[index];
|
|
259
|
+
const batchMentions = Array.from(
|
|
260
|
+
new Set(batch.map((entry) => entry.mentionJid).filter(Boolean)),
|
|
261
|
+
);
|
|
262
|
+
const responseText = buildNoMessageText({
|
|
263
|
+
minMessages,
|
|
264
|
+
periodLabel,
|
|
265
|
+
totalParticipants,
|
|
266
|
+
totalListed,
|
|
267
|
+
batchIndex: index + 1,
|
|
268
|
+
batchTotal: batches.length,
|
|
269
|
+
batchSize: batch.length,
|
|
270
|
+
});
|
|
271
|
+
await sendAndStore(sock,
|
|
272
|
+
remoteJid,
|
|
273
|
+
{ text: responseText, mentions: batchMentions },
|
|
274
|
+
{ quoted: messageInfo, ephemeralExpiration: expirationMessage },
|
|
275
|
+
);
|
|
276
|
+
if (index < batches.length - 1) {
|
|
277
|
+
await sleep(BATCH_DELAY_MS);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} catch (error) {
|
|
281
|
+
logger.error('Erro ao buscar membros sem mensagens:', { error: error.message });
|
|
282
|
+
await sendAndStore(sock,
|
|
283
|
+
remoteJid,
|
|
284
|
+
{ text: `Erro ao buscar membros sem mensagens: ${error.message}` },
|
|
285
|
+
{ quoted: messageInfo, ephemeralExpiration: expirationMessage },
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|