@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,351 @@
1
+ import axios from 'axios';
2
+ import logger from '../utils/logger/loggerModule.js';
3
+ import groupConfigStore from '../store/groupConfigStore.js';
4
+ import { TABLES, findAll } from '../../database/index.js';
5
+ import { getActiveSocket } from './socketState.js';
6
+ import getImageBuffer from '../utils/http/getImageBufferModule.js';
7
+ import { sendAndStore } from './messagePersistenceService.js';
8
+
9
+ const DEFAULT_NEWS_API_URL = 'http://127.0.0.1:3001';
10
+ const NEWS_API_URL = (process.env.NEWS_API_URL || DEFAULT_NEWS_API_URL).replace(/\/+$/, '');
11
+ const MIN_DELAY_MS = 60 * 1000;
12
+ const MAX_DELAY_MS = 120 * 1000;
13
+ const MAX_SENT_IDS = Number(process.env.NEWS_SENT_IDS_LIMIT || 500);
14
+ const LOOP_START_DELAY_MS = 5000;
15
+ const GROUP_UNAVAILABLE_ERROR_PATTERNS = ['item-not-found', 'not-authorized', 'not in group', 'group does not exist', 'recipient not found', 'recipient-unavailable'];
16
+
17
+ const groupLoops = new Map();
18
+
19
+ const getRandomDelayMs = () => {
20
+ const min = MIN_DELAY_MS;
21
+ const max = MAX_DELAY_MS;
22
+ return Math.floor(min + Math.random() * (max - min + 1));
23
+ };
24
+
25
+ const parseConfigValue = (value) => {
26
+ if (value === null || value === undefined) return {};
27
+ if (Buffer.isBuffer(value)) {
28
+ try {
29
+ return JSON.parse(value.toString('utf8'));
30
+ } catch (error) {
31
+ logger.warn('Falha ao fazer parse do config (buffer).', { error: error.message });
32
+ return {};
33
+ }
34
+ }
35
+ if (typeof value === 'string') {
36
+ try {
37
+ return JSON.parse(value);
38
+ } catch (error) {
39
+ logger.warn('Falha ao fazer parse do config (string).', { error: error.message });
40
+ return {};
41
+ }
42
+ }
43
+ if (typeof value === 'object') return value;
44
+ return {};
45
+ };
46
+
47
+ const loadEnabledGroupsFromDb = async () => {
48
+ const enabledGroups = [];
49
+ const limit = 100;
50
+ let offset = 0;
51
+
52
+ while (true) {
53
+ const rows = await findAll(TABLES.GROUP_CONFIGS, limit, offset);
54
+ if (!rows.length) break;
55
+
56
+ for (const row of rows) {
57
+ const config = parseConfigValue(row.config);
58
+ if (config?.newsEnabled) {
59
+ enabledGroups.push(row.id);
60
+ }
61
+ }
62
+
63
+ offset += rows.length;
64
+ if (rows.length < limit) break;
65
+ }
66
+
67
+ return enabledGroups;
68
+ };
69
+
70
+ const normalizeNewsItems = (data) => {
71
+ if (!Array.isArray(data)) return [];
72
+ return data
73
+ .filter((item) => item && typeof item === 'object')
74
+ .map((item) => ({
75
+ id: item.id,
76
+ timestamp: item.timestamp,
77
+ refined: item.refined || {},
78
+ }));
79
+ };
80
+
81
+ const fetchNewsItems = async () => {
82
+ try {
83
+ const response = await axios.get(NEWS_API_URL, { timeout: 15000 });
84
+ return normalizeNewsItems(response.data);
85
+ } catch (error) {
86
+ logger.error('Erro ao buscar noticias da API.', {
87
+ error: error.message,
88
+ url: NEWS_API_URL,
89
+ });
90
+ return [];
91
+ }
92
+ };
93
+
94
+ const buildNewsCaption = (newsItem) => {
95
+ const title = newsItem?.refined?.name || 'Notícia';
96
+ const summary = (newsItem?.refined?.summary || '').trim();
97
+ const url = newsItem?.refined?.url || '';
98
+
99
+ const lines = [`📰 *${title}*`];
100
+ if (summary) {
101
+ lines.push('', summary);
102
+ }
103
+ if (url) {
104
+ lines.push('', `🔗 ${url}`);
105
+ }
106
+ return lines.join('\n').trim();
107
+ };
108
+
109
+ const sortByTimestampAsc = (items) =>
110
+ items.sort((a, b) => {
111
+ const aTime = a?.timestamp ? Date.parse(a.timestamp) : 0;
112
+ const bTime = b?.timestamp ? Date.parse(b.timestamp) : 0;
113
+ if (Number.isNaN(aTime) && Number.isNaN(bTime)) return 0;
114
+ if (Number.isNaN(aTime)) return 1;
115
+ if (Number.isNaN(bTime)) return -1;
116
+ return aTime - bTime;
117
+ });
118
+
119
+ const trimSentIds = (ids) => {
120
+ if (!Array.isArray(ids)) return [];
121
+ if (!Number.isFinite(MAX_SENT_IDS) || MAX_SENT_IDS <= 0) return ids;
122
+ if (ids.length <= MAX_SENT_IDS) return ids;
123
+ return ids.slice(ids.length - MAX_SENT_IDS);
124
+ };
125
+
126
+ const toErrorFragments = (error) => {
127
+ const candidates = [error?.message, error?.data, error?.output?.payload?.message, error?.output?.payload?.error, error?.output?.statusCode, error?.status, error?.cause?.message, error?.cause?.data];
128
+
129
+ return candidates
130
+ .filter((value) => value !== null && value !== undefined)
131
+ .map((value) => {
132
+ if (typeof value === 'string') return value.toLowerCase();
133
+ if (typeof value === 'number') return String(value);
134
+ try {
135
+ return JSON.stringify(value).toLowerCase();
136
+ } catch {
137
+ return String(value).toLowerCase();
138
+ }
139
+ });
140
+ };
141
+
142
+ const isGroupUnavailableError = (error) => {
143
+ const fragments = toErrorFragments(error);
144
+ return fragments.some((fragment) => GROUP_UNAVAILABLE_ERROR_PATTERNS.some((pattern) => fragment.includes(pattern)));
145
+ };
146
+
147
+ const scheduleNextRun = (groupId, delayMs) => {
148
+ const state = groupLoops.get(groupId);
149
+ if (!state || state.stopped) return;
150
+ if (state.timeoutId) clearTimeout(state.timeoutId);
151
+ state.timeoutId = setTimeout(() => {
152
+ processGroupNews(groupId);
153
+ }, delayMs);
154
+ };
155
+
156
+
157
+ const stopGroupLoopInternal = (groupId) => {
158
+ const state = groupLoops.get(groupId);
159
+ if (!state) return;
160
+ if (state.timeoutId) clearTimeout(state.timeoutId);
161
+ state.stopped = true;
162
+ groupLoops.delete(groupId);
163
+ };
164
+
165
+ const processGroupNews = async (groupId) => {
166
+ const state = groupLoops.get(groupId);
167
+ if (!state || state.stopped) return;
168
+ if (state.inFlight) return;
169
+
170
+ state.inFlight = true;
171
+ let shouldSchedule = true;
172
+
173
+ try {
174
+ const config = await groupConfigStore.getGroupConfig(groupId);
175
+ if (!config?.newsEnabled) {
176
+ shouldSchedule = false;
177
+ stopGroupLoopInternal(groupId);
178
+ return;
179
+ }
180
+
181
+ const sock = getActiveSocket();
182
+ if (!sock) {
183
+ const now = Date.now();
184
+ if (!state.lastNotReadyLogAt || now - state.lastNotReadyLogAt > 60_000) {
185
+ state.lastNotReadyLogAt = now;
186
+ logger.debug('Socket nao disponivel para envio de noticias.', { groupId });
187
+ }
188
+ return;
189
+ }
190
+
191
+ const allNews = await fetchNewsItems();
192
+ if (allNews.length === 0) {
193
+ return;
194
+ }
195
+
196
+ const sentIds = new Set(Array.isArray(config.newsSentIds) ? config.newsSentIds : []);
197
+ const unsent = allNews.filter((item) => item?.id && !sentIds.has(item.id));
198
+
199
+ if (unsent.length === 0) {
200
+ return;
201
+ }
202
+
203
+ sortByTimestampAsc(unsent);
204
+ const nextItem = unsent[0];
205
+ const caption = buildNewsCaption(nextItem);
206
+ const imageUrl = nextItem?.refined?.image || '';
207
+ let sent = false;
208
+
209
+ try {
210
+ if (imageUrl && /^https?:\/\//i.test(imageUrl)) {
211
+ let imageBuffer = null;
212
+ try {
213
+ imageBuffer = await getImageBuffer(imageUrl);
214
+ } catch (error) {
215
+ logger.warn('Falha ao baixar imagem da noticia. Enviando texto.', {
216
+ groupId,
217
+ error: error.message,
218
+ imageUrl,
219
+ });
220
+ }
221
+
222
+ if (imageBuffer) {
223
+ try {
224
+ await sendAndStore(sock, groupId, { image: imageBuffer, caption });
225
+ sent = true;
226
+ } catch (error) {
227
+ if (isGroupUnavailableError(error)) {
228
+ throw error;
229
+ }
230
+ logger.warn('Falha ao enviar imagem da noticia. Enviando texto.', {
231
+ groupId,
232
+ error: error.message,
233
+ });
234
+ }
235
+ }
236
+ }
237
+
238
+ if (!sent) {
239
+ await sendAndStore(sock, groupId, { text: caption });
240
+ sent = true;
241
+ }
242
+
243
+ if (sent) {
244
+ sentIds.add(nextItem.id);
245
+ const updatedSentIds = trimSentIds(Array.from(sentIds));
246
+ await groupConfigStore.updateGroupConfig(groupId, {
247
+ newsSentIds: updatedSentIds,
248
+ newsLastSentAt: new Date().toISOString(),
249
+ });
250
+ }
251
+ } catch (error) {
252
+ if (isGroupUnavailableError(error)) {
253
+ shouldSchedule = false;
254
+ stopGroupLoopInternal(groupId);
255
+
256
+ try {
257
+ await groupConfigStore.updateGroupConfig(groupId, { newsEnabled: false });
258
+ } catch (updateError) {
259
+ logger.error('Falha ao desativar noticias para grupo indisponivel.', {
260
+ groupId,
261
+ error: updateError.message,
262
+ });
263
+ }
264
+
265
+ logger.warn('Grupo indisponivel para envio de noticias. Envio automatico desativado.', {
266
+ groupId,
267
+ error: error.message,
268
+ });
269
+ return;
270
+ }
271
+
272
+ logger.error('Erro ao enviar noticia para grupo.', {
273
+ groupId,
274
+ error: error.message,
275
+ });
276
+ }
277
+ } catch (error) {
278
+ logger.error('Erro no processamento de noticias do grupo.', {
279
+ groupId,
280
+ error: error.message,
281
+ });
282
+ } finally {
283
+ state.inFlight = false;
284
+ if (shouldSchedule) {
285
+ scheduleNextRun(groupId, getRandomDelayMs());
286
+ }
287
+ }
288
+ };
289
+
290
+ export const startNewsBroadcastForGroup = (groupId, options = {}) => {
291
+ const existing = groupLoops.get(groupId);
292
+ if (existing && !existing.stopped) {
293
+ return;
294
+ }
295
+
296
+ const initialDelay = typeof options.initialDelayMs === 'number' ? options.initialDelayMs : LOOP_START_DELAY_MS;
297
+
298
+ groupLoops.set(groupId, {
299
+ timeoutId: null,
300
+ inFlight: false,
301
+ stopped: false,
302
+ });
303
+
304
+ scheduleNextRun(groupId, initialDelay);
305
+ };
306
+
307
+ export const stopNewsBroadcastForGroup = (groupId) => {
308
+ stopGroupLoopInternal(groupId);
309
+ };
310
+
311
+ export const syncNewsBroadcastService = async () => {
312
+ try {
313
+ const enabledGroups = await loadEnabledGroupsFromDb();
314
+ if (enabledGroups.length === 0) {
315
+ logger.info('Nenhum grupo com noticias ativadas encontrado.');
316
+ return;
317
+ }
318
+
319
+ enabledGroups.forEach((groupId) => {
320
+ startNewsBroadcastForGroup(groupId);
321
+ });
322
+
323
+ logger.info('Serviço de noticias sincronizado.', {
324
+ groups: enabledGroups.length,
325
+ });
326
+ } catch (error) {
327
+ logger.error('Falha ao sincronizar serviço de noticias.', { error: error.message });
328
+ }
329
+ };
330
+
331
+ export const initializeNewsBroadcastService = async () => syncNewsBroadcastService();
332
+
333
+ export const stopNewsBroadcastService = () => {
334
+ const groupIds = Array.from(groupLoops.keys());
335
+ if (!groupIds.length) {
336
+ return;
337
+ }
338
+
339
+ groupIds.forEach((groupId) => stopGroupLoopInternal(groupId));
340
+ logger.info('Servico de noticias parado.', { groups: groupIds.length });
341
+ };
342
+
343
+ export const getNewsStatusForGroup = async (groupId) => {
344
+ const config = await groupConfigStore.getGroupConfig(groupId);
345
+ const sentCount = Array.isArray(config.newsSentIds) ? config.newsSentIds.length : 0;
346
+ return {
347
+ enabled: Boolean(config.newsEnabled),
348
+ sentCount,
349
+ lastSentAt: config.newsLastSentAt || null,
350
+ };
351
+ };