@kaikybrofc/omnizap-system 2.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/.env.example +534 -0
  2. package/LICENSE +21 -0
  3. package/README.md +431 -0
  4. package/RELEASE-v2.1.2.md +83 -0
  5. package/app/config/adminIdentity.js +87 -0
  6. package/app/config/baileysConfig.js +693 -0
  7. package/app/config/groupUtils.js +388 -0
  8. package/app/connection/socketController.js +992 -0
  9. package/app/controllers/messageController.js +354 -0
  10. package/app/modules/adminModule/groupCommandHandlers.js +1294 -0
  11. package/app/modules/adminModule/groupEventHandlers.js +355 -0
  12. package/app/modules/aiModule/catCommand.js +1006 -0
  13. package/app/modules/broadcastModule/noticeCommand.js +416 -0
  14. package/app/modules/gameModule/diceCommand.js +67 -0
  15. package/app/modules/menuModule/common.js +311 -0
  16. package/app/modules/menuModule/menus.js +59 -0
  17. package/app/modules/playModule/playCommand.js +1615 -0
  18. package/app/modules/quoteModule/quoteCommand.js +851 -0
  19. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +786 -0
  20. package/app/modules/rpgPokemonModule/rpgBattleService.js +2082 -0
  21. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +760 -0
  22. package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
  23. package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +172 -0
  24. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
  25. package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
  26. package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
  27. package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
  28. package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1859 -0
  29. package/app/modules/rpgPokemonModule/rpgPokemonService.js +6738 -0
  30. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
  31. package/app/modules/statsModule/globalRankingCommand.js +65 -0
  32. package/app/modules/statsModule/noMessageCommand.js +288 -0
  33. package/app/modules/statsModule/rankingCommand.js +60 -0
  34. package/app/modules/statsModule/rankingCommon.js +889 -0
  35. package/app/modules/stickerModule/addStickerMetadata.js +239 -0
  36. package/app/modules/stickerModule/convertToWebp.js +390 -0
  37. package/app/modules/stickerModule/stickerCommand.js +454 -0
  38. package/app/modules/stickerModule/stickerConvertCommand.js +156 -0
  39. package/app/modules/stickerModule/stickerTextCommand.js +657 -0
  40. package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
  41. package/app/modules/stickerPackModule/autoPackCollectorService.js +284 -0
  42. package/app/modules/stickerPackModule/semanticReclassificationEngine.js +466 -0
  43. package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +88 -0
  44. package/app/modules/stickerPackModule/semanticThemeClusterService.js +571 -0
  45. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +449 -0
  46. package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
  47. package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +180 -0
  48. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +4078 -0
  49. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +598 -0
  50. package/app/modules/stickerPackModule/stickerClassificationService.js +588 -0
  51. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +102 -0
  52. package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +7506 -0
  53. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1095 -0
  54. package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +108 -0
  55. package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
  56. package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +110 -0
  57. package/app/modules/stickerPackModule/stickerPackItemRepository.js +440 -0
  58. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +337 -0
  59. package/app/modules/stickerPackModule/stickerPackMessageService.js +296 -0
  60. package/app/modules/stickerPackModule/stickerPackRepository.js +442 -0
  61. package/app/modules/stickerPackModule/stickerPackService.js +788 -0
  62. package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +51 -0
  63. package/app/modules/stickerPackModule/stickerPackUtils.js +97 -0
  64. package/app/modules/stickerPackModule/stickerStorageService.js +507 -0
  65. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +233 -0
  66. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +205 -0
  67. package/app/modules/systemMetricsModule/pingCommand.js +421 -0
  68. package/app/modules/tiktokModule/tiktokCommand.js +798 -0
  69. package/app/modules/userModule/userCommand.js +1217 -0
  70. package/app/modules/waifuPicsModule/waifuPicsCommand.js +177 -0
  71. package/app/observability/metrics.js +734 -0
  72. package/app/services/captchaService.js +492 -0
  73. package/app/services/dbWriteQueue.js +572 -0
  74. package/app/services/groupMetadataService.js +279 -0
  75. package/app/services/lidMapService.js +663 -0
  76. package/app/services/messagePersistenceService.js +56 -0
  77. package/app/services/newsBroadcastService.js +351 -0
  78. package/app/services/pokeApiService.js +398 -0
  79. package/app/services/queueUtils.js +57 -0
  80. package/app/services/socketState.js +7 -0
  81. package/app/store/aiPromptStore.js +38 -0
  82. package/app/store/groupConfigStore.js +58 -0
  83. package/app/store/premiumUserStore.js +36 -0
  84. package/app/utils/antiLink/antiLinkModule.js +804 -0
  85. package/app/utils/http/getImageBufferModule.js +18 -0
  86. package/app/utils/json/jsonSanitizer.js +113 -0
  87. package/app/utils/json/jsonSanitizer.test.js +40 -0
  88. package/app/utils/logger/loggerModule.js +262 -0
  89. package/app/utils/systemMetrics/systemMetricsModule.js +91 -0
  90. package/database/index.js +2052 -0
  91. package/database/init.js +516 -0
  92. package/database/migrations/20260203_0001_sticker_packs.sql +54 -0
  93. package/database/migrations/20260210_0003_rpg_pokemon.sql +58 -0
  94. package/database/migrations/20260210_0004_rpg_shiny_biome.sql +9 -0
  95. package/database/migrations/20260210_0005_rpg_missions.sql +14 -0
  96. package/database/migrations/20260210_0006_rpg_world_pokedex_traits.sql +27 -0
  97. package/database/migrations/20260210_0007_rpg_raid_pvp.sql +56 -0
  98. package/database/migrations/20260210_0008_rpg_social_system.sql +195 -0
  99. package/database/migrations/20260211_0009_rpg_social_xp.sql +36 -0
  100. package/database/migrations/20260222_0010_remove_message_xp.sql +2 -0
  101. package/database/migrations/20260226_0011_sticker_asset_classification.sql +17 -0
  102. package/database/migrations/20260226_0012_sticker_pack_engagement.sql +16 -0
  103. package/database/migrations/20260226_0013_sticker_marketplace_intelligence.sql +19 -0
  104. package/database/migrations/20260226_0014_sticker_pack_publish_flow.sql +30 -0
  105. package/database/migrations/20260226_0014_sticker_worker_queues.sql +42 -0
  106. package/database/migrations/20260226_0015_sticker_auto_pack_curation_integrity.sql +18 -0
  107. package/database/migrations/20260226_0016_sticker_web_google_auth_persistence.sql +34 -0
  108. package/database/migrations/20260226_0017_sticker_web_admin_ban.sql +22 -0
  109. package/database/migrations/20260226_0018_sticker_web_admin_moderator.sql +18 -0
  110. package/database/migrations/20260227_0019_sticker_classification_v2_signals.sql +12 -0
  111. package/database/migrations/20260227_0020_semantic_theme_clusters.sql +35 -0
  112. package/docker-compose.yml +103 -0
  113. package/ecosystem.prod.config.cjs +35 -0
  114. package/eslint.config.js +61 -0
  115. package/index.js +437 -0
  116. package/ml/clip_classifier/Dockerfile +16 -0
  117. package/ml/clip_classifier/README.md +120 -0
  118. package/ml/clip_classifier/adaptive_scoring.py +40 -0
  119. package/ml/clip_classifier/classifier.py +654 -0
  120. package/ml/clip_classifier/embedding_store.py +481 -0
  121. package/ml/clip_classifier/env_loader.py +15 -0
  122. package/ml/clip_classifier/llm_label_expander.py +144 -0
  123. package/ml/clip_classifier/main.py +213 -0
  124. package/ml/clip_classifier/requirements.txt +10 -0
  125. package/ml/clip_classifier/similarity_engine.py +74 -0
  126. package/observability/alert-rules.yml +60 -0
  127. package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
  128. package/observability/grafana/dashboards/omnizap-overview.json +170 -0
  129. package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
  130. package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
  131. package/observability/loki-config.yml +38 -0
  132. package/observability/mysql-exporter.cnf +5 -0
  133. package/observability/mysql-setup.sql +46 -0
  134. package/observability/prometheus.yml +32 -0
  135. package/observability/promtail-config.yml +84 -0
  136. package/package.json +109 -0
  137. package/public/api-docs/index.html +144 -0
  138. package/public/css/github-project-panel.css +297 -0
  139. package/public/css/stickers-admin.css +1272 -0
  140. package/public/css/styles.css +671 -0
  141. package/public/index.html +1311 -0
  142. package/public/js/apps/apiDocsApp.js +310 -0
  143. package/public/js/apps/createPackApp.js +2069 -0
  144. package/public/js/apps/homeApp.js +396 -0
  145. package/public/js/apps/stickersAdminApp.js +1744 -0
  146. package/public/js/apps/stickersApp.js +4830 -0
  147. package/public/js/catalog.js +1019 -0
  148. package/public/js/github-panel/components/CommitList.js +34 -0
  149. package/public/js/github-panel/components/ErrorState.js +16 -0
  150. package/public/js/github-panel/components/GithubProjectPanel.js +106 -0
  151. package/public/js/github-panel/components/ReleaseList.js +38 -0
  152. package/public/js/github-panel/components/SkeletonPanel.js +22 -0
  153. package/public/js/github-panel/components/StatCard.js +15 -0
  154. package/public/js/github-panel/index.js +15 -0
  155. package/public/js/github-panel/useGithubRepoData.js +154 -0
  156. package/public/js/github-panel/vendor/react.js +11 -0
  157. package/public/js/runtime/react-runtime.js +19 -0
  158. package/public/licenca/index.html +106 -0
  159. package/public/stickers/admin/index.html +23 -0
  160. package/public/stickers/create/index.html +47 -0
  161. package/public/stickers/index.html +48 -0
  162. package/public/termos-de-uso/index.html +125 -0
  163. package/scripts/cache-bust.mjs +107 -0
  164. package/scripts/deploy.sh +458 -0
  165. package/scripts/github-deploy-notify.mjs +174 -0
  166. package/scripts/release.sh +129 -0
@@ -0,0 +1,398 @@
1
+ import axios from 'axios';
2
+ import logger from '../utils/logger/loggerModule.js';
3
+ import { recordPokeApiCacheHit } from '../observability/metrics.js';
4
+
5
+ const BASE_URL = 'https://pokeapi.co/api/v2';
6
+ const MIN_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
7
+ const CACHE_TTL_MS = Math.max(MIN_CACHE_TTL_MS, Number(process.env.POKEAPI_CACHE_TTL_MS) || MIN_CACHE_TTL_MS);
8
+ const REQUEST_TIMEOUT_MS = Math.max(3_000, Number(process.env.POKEAPI_TIMEOUT_MS) || 10_000);
9
+ const REQUEST_RETRY_ATTEMPTS = Math.max(0, Number(process.env.POKEAPI_RETRY_ATTEMPTS) || 2);
10
+ const REQUEST_RETRY_BASE_DELAY_MS = Math.max(120, Number(process.env.POKEAPI_RETRY_BASE_DELAY_MS) || 350);
11
+ const DNS_FAMILY = [0, 4, 6].includes(Number(process.env.POKEAPI_DNS_FAMILY)) ? Number(process.env.POKEAPI_DNS_FAMILY) : 4;
12
+ const REQUEST_USER_AGENT = String(process.env.POKEAPI_USER_AGENT || 'omnizap-system/2.1 (+https://github.com/Kaikygr/omnizap-system)').trim();
13
+ const DEFAULT_LORE_LANGUAGES = String(process.env.POKEAPI_LORE_LANGS || 'pt-br,pt,en')
14
+ .split(',')
15
+ .map((entry) =>
16
+ String(entry || '')
17
+ .trim()
18
+ .toLowerCase(),
19
+ )
20
+ .filter(Boolean);
21
+
22
+ const sharedCache = globalThis.__omnizapPokeApiCache instanceof Map ? globalThis.__omnizapPokeApiCache : new Map();
23
+ const sharedInflight = globalThis.__omnizapPokeApiInflight instanceof Map ? globalThis.__omnizapPokeApiInflight : new Map();
24
+
25
+ globalThis.__omnizapPokeApiCache = sharedCache;
26
+ globalThis.__omnizapPokeApiInflight = sharedInflight;
27
+
28
+ const normalizeKeyPart = (value) => {
29
+ if (value === null || value === undefined) {
30
+ throw new Error('Identificador inválido para recurso da PokéAPI.');
31
+ }
32
+
33
+ const raw = String(value).trim();
34
+ if (!raw) {
35
+ throw new Error('Identificador vazio para recurso da PokéAPI.');
36
+ }
37
+
38
+ return raw.toLowerCase();
39
+ };
40
+
41
+ export const normalizeApiText = (value) => {
42
+ return String(value || '')
43
+ .replace(/[\n\r\f\t]+/g, ' ')
44
+ .replace(/\s+/g, ' ')
45
+ .trim();
46
+ };
47
+
48
+ const resolveEntryLang = (entry) =>
49
+ String(entry?.language?.name || '')
50
+ .trim()
51
+ .toLowerCase();
52
+
53
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
54
+
55
+ const hasErrorMessage = (value) => typeof value === 'string' && value.trim().length > 0;
56
+
57
+ const extractCauseMessages = (error) => {
58
+ const causes = Array.isArray(error?.cause?.errors) ? error.cause.errors : Array.isArray(error?.errors) ? error.errors : [];
59
+
60
+ const messages = causes.map((entry) => String(entry?.message || '').trim()).filter(Boolean);
61
+ return messages;
62
+ };
63
+
64
+ const summarizeRequestError = (error) => {
65
+ const directMessage = String(error?.message || '').trim();
66
+ const causeMessage = String(error?.cause?.message || '').trim();
67
+ const status = Number(error?.response?.status);
68
+ const code = String(error?.code || error?.cause?.code || '').trim() || null;
69
+ const causeMessages = extractCauseMessages(error);
70
+
71
+ const message = directMessage || causeMessage || causeMessages[0] || (Number.isFinite(status) ? `HTTP ${status}` : null) || (code ? `Erro de rede (${code})` : null) || 'erro-desconhecido';
72
+
73
+ return {
74
+ message,
75
+ code,
76
+ status: Number.isFinite(status) ? status : null,
77
+ causeMessages: causeMessages.slice(0, 3),
78
+ };
79
+ };
80
+
81
+ const isRetryableRequestError = (error) => {
82
+ const status = Number(error?.response?.status);
83
+ if (Number.isFinite(status)) {
84
+ return status === 408 || status === 425 || status === 429 || (status >= 500 && status < 600);
85
+ }
86
+
87
+ const code = String(error?.code || error?.cause?.code || '')
88
+ .trim()
89
+ .toUpperCase();
90
+ if (!code) return false;
91
+ return ['ETIMEDOUT', 'ECONNRESET', 'ECONNABORTED', 'EAI_AGAIN', 'ENETUNREACH', 'EHOSTUNREACH', 'EPIPE'].includes(code);
92
+ };
93
+
94
+ const calculateRetryDelay = (attempt) => {
95
+ const exponential = REQUEST_RETRY_BASE_DELAY_MS * 2 ** Math.max(0, attempt);
96
+ return Math.min(2_500, exponential + Math.floor(Math.random() * 180));
97
+ };
98
+
99
+ const pickEntryByLangPriority = (entries = [], languages = DEFAULT_LORE_LANGUAGES) => {
100
+ const list = Array.isArray(entries) ? entries : [];
101
+ const langPriority = (Array.isArray(languages) ? languages : DEFAULT_LORE_LANGUAGES)
102
+ .map((entry) =>
103
+ String(entry || '')
104
+ .trim()
105
+ .toLowerCase(),
106
+ )
107
+ .filter(Boolean);
108
+ if (!list.length) return null;
109
+ if (!langPriority.length) return list[0] || null;
110
+
111
+ for (const lang of langPriority) {
112
+ const found = list.find((entry) => resolveEntryLang(entry) === lang);
113
+ if (found) return found;
114
+ }
115
+
116
+ const english = list.find((entry) => resolveEntryLang(entry) === 'en');
117
+ if (english) return english;
118
+ return list[0] || null;
119
+ };
120
+
121
+ export const getLocalizedName = (names = [], fallback = null, languages = DEFAULT_LORE_LANGUAGES) => {
122
+ const entry = pickEntryByLangPriority(names, languages);
123
+ const value = normalizeApiText(entry?.name || '');
124
+ return value || normalizeApiText(fallback || '') || null;
125
+ };
126
+
127
+ export const getLocalizedGenus = (genera = [], fallback = null, languages = DEFAULT_LORE_LANGUAGES) => {
128
+ const entry = pickEntryByLangPriority(genera, languages);
129
+ const value = normalizeApiText(entry?.genus || '');
130
+ return value || normalizeApiText(fallback || '') || null;
131
+ };
132
+
133
+ export const getFlavorText = (flavorTextEntries = [], options = {}) => {
134
+ const entries = Array.isArray(flavorTextEntries) ? flavorTextEntries : [];
135
+ if (!entries.length) return null;
136
+
137
+ const languages = Array.isArray(options?.languages) ? options.languages : DEFAULT_LORE_LANGUAGES;
138
+ const preferredVersion = String(options?.version || '')
139
+ .trim()
140
+ .toLowerCase();
141
+ const filtered = preferredVersion
142
+ ? entries.filter(
143
+ (entry) =>
144
+ String(entry?.version?.name || '')
145
+ .trim()
146
+ .toLowerCase() === preferredVersion,
147
+ )
148
+ : entries;
149
+
150
+ const entry = pickEntryByLangPriority(filtered.length ? filtered : entries, languages);
151
+ const value = normalizeApiText(entry?.flavor_text || entry?.text || '');
152
+ return value || null;
153
+ };
154
+
155
+ export const getEffectText = (effectEntries = [], options = {}) => {
156
+ const entries = Array.isArray(effectEntries) ? effectEntries : [];
157
+ if (!entries.length) return null;
158
+ const languages = Array.isArray(options?.languages) ? options.languages : DEFAULT_LORE_LANGUAGES;
159
+ const entry = pickEntryByLangPriority(entries, languages);
160
+ const shortFirst = options?.preferLong ? false : true;
161
+ const primary = shortFirst ? entry?.short_effect : entry?.effect;
162
+ const fallback = shortFirst ? entry?.effect : entry?.short_effect;
163
+ const value = normalizeApiText(primary || fallback || '');
164
+ return value || null;
165
+ };
166
+
167
+ export const getDefaultLoreLanguages = () => [...DEFAULT_LORE_LANGUAGES];
168
+
169
+ const cleanupExpiredEntry = (key, now) => {
170
+ const entry = sharedCache.get(key);
171
+ if (!entry) return null;
172
+ if (entry.expiresAt <= now) {
173
+ sharedCache.delete(key);
174
+ return null;
175
+ }
176
+ return entry.data;
177
+ };
178
+
179
+ const requestResource = async ({ path, cacheKey }) => {
180
+ const now = Date.now();
181
+ const staleEntry = sharedCache.get(cacheKey);
182
+ const staleData = staleEntry?.data || null;
183
+ const cached = cleanupExpiredEntry(cacheKey, now);
184
+ if (cached) {
185
+ recordPokeApiCacheHit();
186
+ return cached;
187
+ }
188
+
189
+ if (sharedInflight.has(cacheKey)) {
190
+ return sharedInflight.get(cacheKey);
191
+ }
192
+
193
+ const requestPromise = (async () => {
194
+ let lastError = null;
195
+ const maxAttempts = Math.max(1, REQUEST_RETRY_ATTEMPTS + 1);
196
+
197
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
198
+ try {
199
+ const response = await axios.get(`${BASE_URL}${path}`, {
200
+ timeout: REQUEST_TIMEOUT_MS,
201
+ family: DNS_FAMILY,
202
+ headers: {
203
+ Accept: 'application/json',
204
+ 'User-Agent': REQUEST_USER_AGENT,
205
+ },
206
+ });
207
+ const data = response?.data;
208
+ sharedCache.set(cacheKey, {
209
+ data,
210
+ expiresAt: Date.now() + CACHE_TTL_MS,
211
+ });
212
+ return data;
213
+ } catch (error) {
214
+ lastError = error;
215
+ const shouldRetry = attempt < maxAttempts - 1 && isRetryableRequestError(error);
216
+ if (shouldRetry) {
217
+ await sleep(calculateRetryDelay(attempt));
218
+ continue;
219
+ }
220
+ break;
221
+ }
222
+ }
223
+
224
+ const details = summarizeRequestError(lastError);
225
+ if (staleData) {
226
+ logger.warn('Falha ao consultar PokéAPI. Usando cache expirado.', {
227
+ path,
228
+ error: details.message,
229
+ code: details.code,
230
+ status: details.status,
231
+ attempts: maxAttempts,
232
+ causeMessages: details.causeMessages,
233
+ });
234
+ return staleData;
235
+ }
236
+
237
+ logger.warn('Falha ao consultar PokéAPI.', {
238
+ path,
239
+ error: details.message,
240
+ code: details.code,
241
+ status: details.status,
242
+ attempts: maxAttempts,
243
+ causeMessages: details.causeMessages,
244
+ });
245
+
246
+ if (!lastError || typeof lastError !== 'object') {
247
+ const normalizedError = new Error(details.message);
248
+ normalizedError.code = details.code;
249
+ normalizedError.status = details.status;
250
+ normalizedError.pokeApiMeta = {
251
+ code: details.code,
252
+ status: details.status,
253
+ attempts: maxAttempts,
254
+ causeMessages: details.causeMessages,
255
+ };
256
+ throw normalizedError;
257
+ }
258
+
259
+ if (!hasErrorMessage(lastError.message)) {
260
+ lastError.message = details.message;
261
+ }
262
+ lastError.pokeApiMeta = {
263
+ code: details.code,
264
+ status: details.status,
265
+ attempts: maxAttempts,
266
+ causeMessages: details.causeMessages,
267
+ };
268
+ throw lastError;
269
+ })().finally(() => {
270
+ sharedInflight.delete(cacheKey);
271
+ });
272
+
273
+ sharedInflight.set(cacheKey, requestPromise);
274
+ return requestPromise;
275
+ };
276
+
277
+ const getNamedResource = async (resource, idOrName) => {
278
+ const normalized = normalizeKeyPart(idOrName);
279
+ return requestResource({
280
+ path: `/${resource}/${encodeURIComponent(normalized)}`,
281
+ cacheKey: `${resource}:${normalized}`,
282
+ });
283
+ };
284
+
285
+ export const getResourceList = async ({ resource, limit = 20, offset = 0 }) => {
286
+ const safeLimit = Math.max(1, Math.min(200, Number(limit) || 20));
287
+ const safeOffset = Math.max(0, Number(offset) || 0);
288
+ const cacheKey = `list:${resource}:${safeLimit}:${safeOffset}`;
289
+ const query = `?limit=${encodeURIComponent(String(safeLimit))}&offset=${encodeURIComponent(String(safeOffset))}`;
290
+ return requestResource({
291
+ path: `/${resource}${query}`,
292
+ cacheKey,
293
+ });
294
+ };
295
+
296
+ export const getPokemonImage = (pokemonApiResponse, options = {}) => {
297
+ const shinyPreferred = Boolean(options?.shiny);
298
+ if (shinyPreferred) {
299
+ const shinyFront = pokemonApiResponse?.sprites?.front_shiny;
300
+ if (typeof shinyFront === 'string' && shinyFront.trim()) {
301
+ return shinyFront.trim();
302
+ }
303
+ }
304
+
305
+ const officialArtwork = pokemonApiResponse?.sprites?.other?.['official-artwork']?.front_default;
306
+ if (typeof officialArtwork === 'string' && officialArtwork.trim()) {
307
+ return officialArtwork.trim();
308
+ }
309
+
310
+ const frontDefault = pokemonApiResponse?.sprites?.front_default;
311
+ if (typeof frontDefault === 'string' && frontDefault.trim()) {
312
+ return frontDefault.trim();
313
+ }
314
+
315
+ return null;
316
+ };
317
+
318
+ export const getPokemon = async (idOrName) => {
319
+ return getNamedResource('pokemon', idOrName);
320
+ };
321
+
322
+ export const getMove = async (idOrName) => {
323
+ return getNamedResource('move', idOrName);
324
+ };
325
+
326
+ export const getType = async (name) => {
327
+ return getNamedResource('type', name);
328
+ };
329
+
330
+ export const getSpecies = async (id) => {
331
+ const normalized = normalizeKeyPart(id);
332
+ return requestResource({
333
+ path: `/pokemon-species/${encodeURIComponent(normalized)}`,
334
+ cacheKey: `species:${normalized}`,
335
+ });
336
+ };
337
+
338
+ export const getEvolutionChain = async (id) => {
339
+ return getNamedResource('evolution-chain', id);
340
+ };
341
+
342
+ export const getItem = async (idOrName) => getNamedResource('item', idOrName);
343
+
344
+ export const getItemCategory = async (idOrName) => getNamedResource('item-category', idOrName);
345
+
346
+ export const getItemPocket = async (idOrName) => getNamedResource('item-pocket', idOrName);
347
+
348
+ export const getMachine = async (idOrName) => getNamedResource('machine', idOrName);
349
+
350
+ export const getBerry = async (idOrName) => getNamedResource('berry', idOrName);
351
+
352
+ export const getBerryFlavor = async (idOrName) => getNamedResource('berry-flavor', idOrName);
353
+
354
+ export const getRegion = async (idOrName) => getNamedResource('region', idOrName);
355
+
356
+ export const getLocation = async (idOrName) => getNamedResource('location', idOrName);
357
+
358
+ export const getLocationArea = async (idOrName) => getNamedResource('location-area', idOrName);
359
+
360
+ export const getPokedex = async (idOrName) => getNamedResource('pokedex', idOrName);
361
+
362
+ export const getGeneration = async (idOrName) => getNamedResource('generation', idOrName);
363
+
364
+ export const getNature = async (idOrName) => getNamedResource('nature', idOrName);
365
+
366
+ export const getAbility = async (idOrName) => getNamedResource('ability', idOrName);
367
+
368
+ export const getCharacteristic = async (idOrName) => getNamedResource('characteristic', idOrName);
369
+
370
+ export default {
371
+ getPokemon,
372
+ getMove,
373
+ getType,
374
+ getSpecies,
375
+ getEvolutionChain,
376
+ getItem,
377
+ getItemCategory,
378
+ getItemPocket,
379
+ getMachine,
380
+ getBerry,
381
+ getBerryFlavor,
382
+ getRegion,
383
+ getLocation,
384
+ getLocationArea,
385
+ getPokedex,
386
+ getGeneration,
387
+ getNature,
388
+ getAbility,
389
+ getCharacteristic,
390
+ getLocalizedName,
391
+ getLocalizedGenus,
392
+ getFlavorText,
393
+ getEffectText,
394
+ getDefaultLoreLanguages,
395
+ normalizeApiText,
396
+ getResourceList,
397
+ getPokemonImage,
398
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Cria placeholders SQL do tipo "(?, ?, ?), (?, ?, ?)".
3
+ * @param {number} rows
4
+ * @param {number} cols
5
+ * @returns {string}
6
+ */
7
+ export const buildPlaceholders = (rows, cols) =>
8
+ Array.from({ length: rows }, () => `(${Array(cols).fill('?').join(', ')})`).join(', ');
9
+
10
+ /**
11
+ * Cria placeholders repetindo um template por linha.
12
+ * @param {number} rows
13
+ * @param {string} rowTemplate
14
+ * @returns {string}
15
+ */
16
+ export const buildRowPlaceholders = (rows, rowTemplate) =>
17
+ Array.from({ length: rows }, () => rowTemplate).join(', ');
18
+
19
+ /**
20
+ * Cria um executor de flush com controle de concorrencia e re-try imediato.
21
+ * @param {object} params
22
+ * @param {() => Promise<void>} params.onFlush
23
+ * @param {(error: Error) => void} [params.onError]
24
+ * @param {() => void} [params.onFinally]
25
+ * @returns {{ run: () => Promise<void>, isInProgress: () => boolean }}
26
+ */
27
+ export const createFlushRunner = ({ onFlush, onError, onFinally }) => {
28
+ let inProgress = false;
29
+ let requested = false;
30
+
31
+ const run = async () => {
32
+ if (inProgress) {
33
+ requested = true;
34
+ return;
35
+ }
36
+ inProgress = true;
37
+ try {
38
+ await onFlush();
39
+ } catch (error) {
40
+ if (onError) onError(error);
41
+ } finally {
42
+ inProgress = false;
43
+ if (onFinally) onFinally();
44
+ if (requested) {
45
+ requested = false;
46
+ setImmediate(() => {
47
+ run().catch(() => {});
48
+ });
49
+ }
50
+ }
51
+ };
52
+
53
+ return {
54
+ run,
55
+ isInProgress: () => inProgress,
56
+ };
57
+ };
@@ -0,0 +1,7 @@
1
+ let activeSocket = null;
2
+
3
+ export const setActiveSocket = (socket) => {
4
+ activeSocket = socket;
5
+ };
6
+
7
+ export const getActiveSocket = () => activeSocket;
@@ -0,0 +1,38 @@
1
+ import groupConfigStore from './groupConfigStore.js';
2
+
3
+ const PROMPT_CONFIG_ID = 'system:ai_prompts';
4
+
5
+ const normalizeMap = (map) => (map && typeof map === 'object' ? map : {});
6
+
7
+ const aiPromptStore = {
8
+ getAllPrompts: async function () {
9
+ const config = await groupConfigStore.getGroupConfig(PROMPT_CONFIG_ID);
10
+ return normalizeMap(config.prompts);
11
+ },
12
+
13
+ getPrompt: async function (jid) {
14
+ if (!jid) return null;
15
+ const prompts = await this.getAllPrompts();
16
+ return prompts[jid] || null;
17
+ },
18
+
19
+ setPrompt: async function (jid, prompt) {
20
+ if (!jid) return null;
21
+ const prompts = await this.getAllPrompts();
22
+ prompts[jid] = prompt;
23
+ await groupConfigStore.updateGroupConfig(PROMPT_CONFIG_ID, { prompts });
24
+ return prompt;
25
+ },
26
+
27
+ clearPrompt: async function (jid) {
28
+ if (!jid) return null;
29
+ const prompts = await this.getAllPrompts();
30
+ if (prompts[jid]) {
31
+ delete prompts[jid];
32
+ await groupConfigStore.updateGroupConfig(PROMPT_CONFIG_ID, { prompts });
33
+ }
34
+ return true;
35
+ },
36
+ };
37
+
38
+ export default aiPromptStore;
@@ -0,0 +1,58 @@
1
+ import logger from '../utils/logger/loggerModule.js';
2
+ import { findById, upsert } from '../../database/index.js';
3
+
4
+ const groupConfigStore = {
5
+ /**
6
+ * Recupera a configuracao de um grupo especifico.
7
+ * @param {string} groupId - O JID do grupo.
8
+ * @returns {object} A configuracao do grupo, ou um objeto vazio se nao encontrado.
9
+ */
10
+ getGroupConfig: async function (groupId) {
11
+ try {
12
+ const record = await findById('group_configs', groupId);
13
+ if (!record || record.config === null || record.config === undefined) {
14
+ return {};
15
+ }
16
+ if (Buffer.isBuffer(record.config)) {
17
+ return JSON.parse(record.config.toString('utf8'));
18
+ }
19
+ if (typeof record.config === 'string') {
20
+ return JSON.parse(record.config);
21
+ }
22
+ return record.config || {};
23
+ } catch (error) {
24
+ logger.error('Error loading group configuration from DB:', {
25
+ error: error.message,
26
+ groupId,
27
+ });
28
+ return {};
29
+ }
30
+ },
31
+
32
+ /**
33
+ * Atualiza a configuracao de um grupo especifico.
34
+ * @param {string} groupId - O JID do grupo.
35
+ * @param {object} newConfig - O novo objeto de configuracao para mesclar.
36
+ * @param {string} [newConfig.welcomeMedia] - Caminho opcional para midia de boas-vindas.
37
+ * @param {string} [newConfig.farewellMedia] - Caminho opcional para midia de despedida.
38
+ */
39
+ updateGroupConfig: async function (groupId, newConfig) {
40
+ const currentConfig = await this.getGroupConfig(groupId);
41
+ const updatedConfig = { ...currentConfig, ...newConfig };
42
+ try {
43
+ await upsert('group_configs', {
44
+ id: groupId,
45
+ config: JSON.stringify(updatedConfig),
46
+ });
47
+ return updatedConfig;
48
+ } catch (error) {
49
+ logger.error('Error updating group configuration in DB:', {
50
+ error: error.message,
51
+ groupId,
52
+ });
53
+ throw error;
54
+ }
55
+ },
56
+ };
57
+
58
+ export default groupConfigStore;
@@ -0,0 +1,36 @@
1
+ import groupConfigStore from './groupConfigStore.js';
2
+
3
+ const PREMIUM_CONFIG_ID = 'system:premium_users';
4
+
5
+ const normalizeList = (list) =>
6
+ Array.from(new Set((Array.isArray(list) ? list : []).filter(Boolean)));
7
+
8
+ const premiumUserStore = {
9
+ getPremiumUsers: async function () {
10
+ const config = await groupConfigStore.getGroupConfig(PREMIUM_CONFIG_ID);
11
+ return normalizeList(config.premiumUsers);
12
+ },
13
+
14
+ setPremiumUsers: async function (premiumUsers) {
15
+ const normalized = normalizeList(premiumUsers);
16
+ await groupConfigStore.updateGroupConfig(PREMIUM_CONFIG_ID, { premiumUsers: normalized });
17
+ return normalized;
18
+ },
19
+
20
+ addPremiumUsers: async function (usersToAdd) {
21
+ const current = await this.getPremiumUsers();
22
+ const updated = normalizeList([...current, ...usersToAdd]);
23
+ await groupConfigStore.updateGroupConfig(PREMIUM_CONFIG_ID, { premiumUsers: updated });
24
+ return updated;
25
+ },
26
+
27
+ removePremiumUsers: async function (usersToRemove) {
28
+ const current = await this.getPremiumUsers();
29
+ const removeSet = new Set(usersToRemove);
30
+ const updated = current.filter((jid) => !removeSet.has(jid));
31
+ await groupConfigStore.updateGroupConfig(PREMIUM_CONFIG_ID, { premiumUsers: updated });
32
+ return updated;
33
+ },
34
+ };
35
+
36
+ export default premiumUserStore;