@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,804 @@
1
+ import { URL } from 'node:url';
2
+ import { isUserAdmin, updateGroupParticipants } from '../../config/groupUtils.js';
3
+ import { getJidUser } from '../../config/baileysConfig.js';
4
+ import groupConfigStore from '../../store/groupConfigStore.js';
5
+ import logger from '../logger/loggerModule.js';
6
+ import { sendAndStore } from '../../services/messagePersistenceService.js';
7
+
8
+ /**
9
+ * Base de redes conhecidas e seus domínios oficiais para permitir por categoria.
10
+ * @type {Record<string, string[]>}
11
+ */
12
+ export const KNOWN_NETWORKS = {
13
+ youtube: ['youtube.com', 'youtu.be', 'music.youtube.com', 'm.youtube.com', 'shorts.youtube.com', 'youtube-nocookie.com'],
14
+ instagram: ['instagram.com', 'instagr.am'],
15
+ facebook: ['facebook.com', 'fb.com', 'fb.watch', 'm.facebook.com', 'l.facebook.com'],
16
+ tiktok: ['tiktok.com', 'vm.tiktok.com', 'vt.tiktok.com'],
17
+ twitter: ['twitter.com', 'x.com', 't.co', 'mobile.twitter.com'],
18
+ linkedin: ['linkedin.com', 'lnkd.in'],
19
+ twitch: ['twitch.tv', 'clips.twitch.tv'],
20
+ discord: ['discord.com', 'discord.gg', 'discordapp.com', 'discordapp.net'],
21
+ whatsapp: ['chat.whatsapp.com', 'wa.me'],
22
+ telegram: ['t.me', 'telegram.me', 'telesco.pe'],
23
+ reddit: ['reddit.com', 'redd.it'],
24
+ pinterest: ['pinterest.com', 'pin.it'],
25
+ snapchat: ['snapchat.com', 'snap.com'],
26
+ kwai: ['kwai.com', 'kw.ai'],
27
+ likee: ['likee.video'],
28
+ vimeo: ['vimeo.com', 'player.vimeo.com'],
29
+ dailymotion: ['dailymotion.com', 'dai.ly'],
30
+ rumble: ['rumble.com'],
31
+ kick: ['kick.com'],
32
+ soundcloud: ['soundcloud.com'],
33
+ spotify: ['spotify.com', 'open.spotify.com'],
34
+ deezer: ['deezer.com', 'deezer.page.link'],
35
+ applemusic: ['music.apple.com'],
36
+ shazam: ['shazam.com'],
37
+ bandcamp: ['bandcamp.com'],
38
+ amazonmusic: ['music.amazon.com'],
39
+ imdb: ['imdb.com'],
40
+ letterboxd: ['letterboxd.com'],
41
+ goodreads: ['goodreads.com'],
42
+ medium: ['medium.com'],
43
+ substack: ['substack.com'],
44
+ behance: ['behance.net'],
45
+ dribbble: ['dribbble.com'],
46
+ deviantart: ['deviantart.com'],
47
+ artstation: ['artstation.com'],
48
+ figma: ['figma.com', 'figma.io'],
49
+ github: ['github.com', 'gist.github.com', 'github.io'],
50
+ gitlab: ['gitlab.com'],
51
+ bitbucket: ['bitbucket.org'],
52
+ npm: ['npmjs.com'],
53
+ pypi: ['pypi.org'],
54
+ stackoverflow: ['stackoverflow.com', 'stackexchange.com'],
55
+ quora: ['quora.com'],
56
+ stackshare: ['stackshare.io'],
57
+ producthunt: ['producthunt.com'],
58
+ hackernews: ['news.ycombinator.com'],
59
+ google: ['google.com', 'goo.gl', 'g.co', 'maps.google.com'],
60
+ maps: ['google.com', 'maps.google.com', 'goo.gl', 'g.page'],
61
+ playstore: ['play.google.com'],
62
+ appstore: ['apps.apple.com'],
63
+ steam: ['steamcommunity.com', 'store.steampowered.com', 'steamdb.info'],
64
+ epicgames: ['epicgames.com'],
65
+ discordbots: ['top.gg', 'discords.com', 'discordbotlist.com'],
66
+ cloudflare: ['cloudflare.com', 'pages.dev', 'workers.dev'],
67
+ heroku: ['heroku.com', 'herokuapp.com'],
68
+ vercel: ['vercel.app', 'vercel.com'],
69
+ netlify: ['netlify.app', 'netlify.com'],
70
+ firebase: ['firebase.google.com', 'web.app'],
71
+ hostinger: ['hostinger.com'],
72
+ wix: ['wix.com', 'wixsite.com'],
73
+ squarespace: ['squarespace.com'],
74
+ wordpress: ['wordpress.com', 'wordpress.org'],
75
+ blogger: ['blogger.com', 'blogspot.com'],
76
+ tumblr: ['tumblr.com'],
77
+ weibo: ['weibo.com'],
78
+ vk: ['vk.com'],
79
+ okru: ['ok.ru'],
80
+ line: ['line.me'],
81
+ wechat: ['wechat.com', 'weixin.qq.com', 'we.chat'],
82
+ qq: ['qq.com'],
83
+ signal: ['signal.org'],
84
+ skype: ['skype.com'],
85
+ slack: ['slack.com'],
86
+ zoom: ['zoom.us', 'zoom.com'],
87
+ meet: ['meet.google.com'],
88
+ teams: ['microsoft.com', 'teams.microsoft.com'],
89
+ canva: ['canva.com'],
90
+ notion: ['notion.so', 'notion.site'],
91
+ trello: ['trello.com'],
92
+ asana: ['asana.com'],
93
+ monday: ['monday.com'],
94
+ clickup: ['clickup.com'],
95
+ airtable: ['airtable.com'],
96
+ coursera: ['coursera.org'],
97
+ udemy: ['udemy.com'],
98
+ udacity: ['udacity.com'],
99
+ edx: ['edx.org'],
100
+ khanacademy: ['khanacademy.org'],
101
+ duolingo: ['duolingo.com'],
102
+ roblox: ['roblox.com'],
103
+ minecraft: ['minecraft.net', 'minecraft.net.br'],
104
+ valorant: ['valorant.com'],
105
+ riot: ['riotgames.com'],
106
+ leagueoflegends: ['leagueoflegends.com'],
107
+ dota2: ['dota2.com'],
108
+ csgo: ['counter-strike.net'],
109
+ };
110
+
111
+ /**
112
+ * Delimitadores básicos para tokenização manual (sem regex).
113
+ */
114
+ const WHITESPACE_CHARS = new Set([' ', '\n', '\r', '\t', '\f', '\v']);
115
+ const EDGE_PUNCTUATION_CHARS = new Set([',', '!', '?', ';', ':', ')', '(', '[', ']', '{', '}', '<', '>', '"', "'", '`', '…']);
116
+ const TOKEN_SEPARATOR_CHARS = new Set([',', ';', '|']);
117
+ const HOST_TERMINATORS = new Set(['/', '?', '#', ':', '\\', ',', ';']);
118
+ const URL_HINTS = ['https://', 'http://', 'www.'];
119
+ const STRICT_TLD_SUFFIXES = new Set([
120
+ 'com',
121
+ 'net',
122
+ 'org',
123
+ 'edu',
124
+ 'gov',
125
+ 'mil',
126
+ 'io',
127
+ 'me',
128
+ 'tv',
129
+ 'co',
130
+ 'cc',
131
+ 'gg',
132
+ 'gl',
133
+ 'ly',
134
+ 'so',
135
+ 'br',
136
+ 'us',
137
+ 'uk',
138
+ 'eu',
139
+ 'de',
140
+ 'fr',
141
+ 'es',
142
+ 'pt',
143
+ 'it',
144
+ 'nl',
145
+ 'be',
146
+ 'ch',
147
+ 'at',
148
+ 'se',
149
+ 'no',
150
+ 'fi',
151
+ 'dk',
152
+ 'ie',
153
+ 'pl',
154
+ 'cz',
155
+ 'sk',
156
+ 'hu',
157
+ 'ro',
158
+ 'bg',
159
+ 'gr',
160
+ 'ru',
161
+ 'ua',
162
+ 'tr',
163
+ 'il',
164
+ 'ae',
165
+ 'sa',
166
+ 'qa',
167
+ 'eg',
168
+ 'ma',
169
+ 'tn',
170
+ 'dz',
171
+ 'za',
172
+ 'ng',
173
+ 'ke',
174
+ 'gh',
175
+ 'in',
176
+ 'pk',
177
+ 'bd',
178
+ 'lk',
179
+ 'cn',
180
+ 'jp',
181
+ 'kr',
182
+ 'tw',
183
+ 'hk',
184
+ 'sg',
185
+ 'my',
186
+ 'th',
187
+ 'vn',
188
+ 'ph',
189
+ 'id',
190
+ 'au',
191
+ 'nz',
192
+ 'ca',
193
+ 'mx',
194
+ 'ar',
195
+ 'cl',
196
+ 'pe',
197
+ 'uy',
198
+ 'py',
199
+ 'bo',
200
+ 'ec',
201
+ 've',
202
+ 'do',
203
+ 'cu',
204
+ 'pa',
205
+ 'cr',
206
+ 'gt',
207
+ 'hn',
208
+ 'ni',
209
+ 'sv',
210
+ 'pr',
211
+ 'com.br',
212
+ 'net.br',
213
+ 'org.br',
214
+ 'gov.br',
215
+ 'edu.br',
216
+ 'jus.br',
217
+ 'mil.br',
218
+ 'co.uk',
219
+ 'org.uk',
220
+ 'gov.uk',
221
+ 'ac.uk',
222
+ 'co.jp',
223
+ 'ne.jp',
224
+ 'or.jp',
225
+ 'go.jp',
226
+ 'ac.jp',
227
+ 'com.au',
228
+ 'net.au',
229
+ 'org.au',
230
+ 'edu.au',
231
+ 'gov.au',
232
+ 'com.mx',
233
+ 'com.ar',
234
+ 'com.co',
235
+ 'com.pe',
236
+ 'com.tr',
237
+ 'com.sg',
238
+ 'com.my',
239
+ 'com.ph',
240
+ 'co.in',
241
+ 'firm.in',
242
+ 'net.in',
243
+ 'org.in',
244
+ 'gen.in',
245
+ 'ind.in',
246
+ 'co.id',
247
+ 'or.id',
248
+ 'go.id',
249
+ 'web.id',
250
+ 'co.za',
251
+ 'org.za',
252
+ 'net.za',
253
+ 'com.ng',
254
+ 'com.gh',
255
+ 'com.eg',
256
+ 'com.sa',
257
+ 'com.qa',
258
+ 'com.ae',
259
+ 'page.link',
260
+ 'g.page',
261
+ ]);
262
+ const EXTRA_TLD_SUFFIXES = new Set([
263
+ 'ai',
264
+ 'app',
265
+ 'dev',
266
+ 'xyz',
267
+ 'site',
268
+ 'online',
269
+ 'store',
270
+ 'shop',
271
+ 'blog',
272
+ 'tech',
273
+ 'cloud',
274
+ 'digital',
275
+ 'live',
276
+ 'media',
277
+ 'news',
278
+ 'one',
279
+ 'top',
280
+ 'club',
281
+ 'vip',
282
+ 'fun',
283
+ 'games',
284
+ 'game',
285
+ 'space',
286
+ 'world',
287
+ 'today',
288
+ 'agency',
289
+ 'email',
290
+ 'center',
291
+ 'company',
292
+ 'group',
293
+ 'solutions',
294
+ 'systems',
295
+ 'services',
296
+ 'network',
297
+ 'social',
298
+ 'design',
299
+ 'studio',
300
+ 'photo',
301
+ 'video',
302
+ 'audio',
303
+ 'music',
304
+ 'art',
305
+ 'wiki',
306
+ 'finance',
307
+ 'capital',
308
+ 'money',
309
+ 'loans',
310
+ 'insurance',
311
+ 'legal',
312
+ 'law',
313
+ 'health',
314
+ 'care',
315
+ 'clinic',
316
+ 'dental',
317
+ 'academy',
318
+ 'school',
319
+ 'college',
320
+ 'university',
321
+ 'education',
322
+ 'training',
323
+ 'support',
324
+ 'chat',
325
+ 'forum',
326
+ 'community',
327
+ 'events',
328
+ 'travel',
329
+ 'tours',
330
+ 'hotel',
331
+ 'homes',
332
+ 'house',
333
+ 'auto',
334
+ 'cars',
335
+ 'bike',
336
+ 'food',
337
+ 'restaurant',
338
+ 'cafe',
339
+ 'bar',
340
+ 'pizza',
341
+ 'delivery',
342
+ 'fashion',
343
+ 'beauty',
344
+ 'style',
345
+ 'fit',
346
+ 'fitness',
347
+ 'sports',
348
+ 'download',
349
+ ]);
350
+ const ANY_TLD_SUFFIXES = new Set([...STRICT_TLD_SUFFIXES, ...EXTRA_TLD_SUFFIXES]);
351
+
352
+ /**
353
+ * Tokeniza texto por espaço/quebra de linha sem regex.
354
+ * @param {string} text
355
+ * @returns {string[]}
356
+ */
357
+ const tokenizeText = (text) => {
358
+ if (!text) return [];
359
+ const tokens = [];
360
+ let currentToken = '';
361
+
362
+ for (const char of text) {
363
+ if (WHITESPACE_CHARS.has(char)) {
364
+ if (currentToken) {
365
+ tokens.push(currentToken);
366
+ currentToken = '';
367
+ }
368
+ continue;
369
+ }
370
+ currentToken += char;
371
+ }
372
+
373
+ if (currentToken) tokens.push(currentToken);
374
+ return tokens;
375
+ };
376
+
377
+ /**
378
+ * Divide tokens compostos (site.com,site2.com|site3.com) sem regex.
379
+ * @param {string} token
380
+ * @returns {string[]}
381
+ */
382
+ const splitCompositeToken = (token) => {
383
+ const parts = [];
384
+ let currentPart = '';
385
+
386
+ for (let i = 0; i < token.length; i += 1) {
387
+ const char = token[i];
388
+ const isArrowSeparator = char === '>' && i > 0 && token[i - 1] === '-';
389
+
390
+ if (TOKEN_SEPARATOR_CHARS.has(char) || isArrowSeparator) {
391
+ if (isArrowSeparator && currentPart.endsWith('-')) {
392
+ currentPart = currentPart.slice(0, -1);
393
+ }
394
+ if (currentPart) {
395
+ parts.push(currentPart);
396
+ currentPart = '';
397
+ }
398
+ continue;
399
+ }
400
+ currentPart += char;
401
+ }
402
+
403
+ if (currentPart) parts.push(currentPart);
404
+ return parts;
405
+ };
406
+
407
+ /**
408
+ * Remove pontuação comum das bordas do token.
409
+ * @param {string} token
410
+ * @returns {string}
411
+ */
412
+ const stripEdgePunctuation = (token) => {
413
+ let start = 0;
414
+ let end = token.length;
415
+
416
+ while (start < end && EDGE_PUNCTUATION_CHARS.has(token[start])) start += 1;
417
+ while (end > start && EDGE_PUNCTUATION_CHARS.has(token[end - 1])) end -= 1;
418
+
419
+ return token.slice(start, end);
420
+ };
421
+
422
+ const isAsciiLetter = (char) => char >= 'a' && char <= 'z';
423
+ const isDigit = (char) => char >= '0' && char <= '9';
424
+ const isDomainLabelChar = (char) => isAsciiLetter(char) || isDigit(char) || char === '-';
425
+
426
+ /**
427
+ * Normaliza host removendo "www." e pontos nas bordas.
428
+ * @param {string} host
429
+ * @returns {string}
430
+ */
431
+ const normalizeHost = (host) => {
432
+ if (!host) return '';
433
+ let normalized = host.toLowerCase();
434
+
435
+ while (normalized.startsWith('.')) {
436
+ normalized = normalized.slice(1);
437
+ }
438
+ while (normalized.endsWith('.')) {
439
+ normalized = normalized.slice(0, -1);
440
+ }
441
+ while (normalized.startsWith('www.')) {
442
+ normalized = normalized.slice(4);
443
+ }
444
+
445
+ return normalized;
446
+ };
447
+
448
+ /**
449
+ * Retorna quantos labels formam o TLD conhecido (1, 2 ou 3).
450
+ * @param {string[]} labels
451
+ * @param {Set<string>} suffixSet
452
+ * @returns {number}
453
+ */
454
+ const getKnownTldLabelCount = (labels, suffixSet) => {
455
+ if (labels.length >= 3) {
456
+ const lastThree = labels.slice(-3).join('.');
457
+ if (suffixSet.has(lastThree)) return 3;
458
+ }
459
+ if (labels.length >= 2) {
460
+ const lastTwo = labels.slice(-2).join('.');
461
+ if (suffixSet.has(lastTwo)) return 2;
462
+ }
463
+ const lastOne = labels[labels.length - 1];
464
+ if (suffixSet.has(lastOne)) return 1;
465
+ return 0;
466
+ };
467
+
468
+ /**
469
+ * Conta labels de TLD somente na lista strict.
470
+ * @param {string[]} labels
471
+ * @returns {number}
472
+ */
473
+ const getStrictTldLabelCount = (labels) => getKnownTldLabelCount(labels, STRICT_TLD_SUFFIXES);
474
+
475
+ /**
476
+ * Conta labels de TLD aceitando strict + extra.
477
+ * @param {string[]} labels
478
+ * @returns {number}
479
+ */
480
+ const getAnyTldLabelCount = (labels) => getKnownTldLabelCount(labels, ANY_TLD_SUFFIXES);
481
+
482
+ /**
483
+ * Extrai o root registrável com base nos TLDs strict.
484
+ * @param {string} domain
485
+ * @returns {string}
486
+ */
487
+ const getStrictRegistrableRootDomain = (domain) => {
488
+ const labels = domain.split('.');
489
+ const tldLabelCount = getStrictTldLabelCount(labels);
490
+ if (tldLabelCount === 0 || labels.length <= tldLabelCount) return '';
491
+ return labels.slice(-(tldLabelCount + 1)).join('.');
492
+ };
493
+
494
+ /**
495
+ * Valida a estrutura do domínio sem regex.
496
+ * @param {string} domain
497
+ * @returns {boolean}
498
+ */
499
+ const isValidDomainStructure = (domain) => {
500
+ if (!domain || domain.length > 253 || !domain.includes('.')) return false;
501
+ const labels = domain.split('.');
502
+
503
+ for (const label of labels) {
504
+ if (!label || label.length > 63) return false;
505
+ if (label[0] === '-' || label[label.length - 1] === '-') return false;
506
+
507
+ for (const char of label) {
508
+ if (!isDomainLabelChar(char)) return false;
509
+ }
510
+ }
511
+
512
+ return true;
513
+ };
514
+
515
+ /**
516
+ * Verifica se o domínio termina com TLD/sufixo conhecido.
517
+ * @param {string} domain
518
+ * @returns {boolean}
519
+ */
520
+ const hasStrictKnownTldSuffix = (domain) => {
521
+ const labels = domain.split('.');
522
+ return getStrictTldLabelCount(labels) > 0;
523
+ };
524
+
525
+ /**
526
+ * Verifica se o domínio termina com TLD/sufixo conhecido (strict + extra).
527
+ * @param {string} domain
528
+ * @returns {boolean}
529
+ */
530
+ const hasAnyKnownTldSuffix = (domain) => {
531
+ const labels = domain.split('.');
532
+ return getAnyTldLabelCount(labels) > 0;
533
+ };
534
+
535
+ const KNOWN_NETWORK_EXACT_DOMAINS = new Set();
536
+ const KNOWN_NETWORK_SUBDOMAIN_ROOTS = new Set();
537
+
538
+ for (const domains of Object.values(KNOWN_NETWORKS)) {
539
+ for (const domain of domains) {
540
+ const normalizedDomain = domain.toLowerCase();
541
+ KNOWN_NETWORK_EXACT_DOMAINS.add(normalizedDomain);
542
+
543
+ const rootDomain = getStrictRegistrableRootDomain(normalizedDomain);
544
+ if (rootDomain) {
545
+ KNOWN_NETWORK_SUBDOMAIN_ROOTS.add(rootDomain);
546
+ }
547
+ }
548
+ }
549
+
550
+ /**
551
+ * Verifica domínios oficiais já mapeados em KNOWN_NETWORKS.
552
+ * @param {string} domain
553
+ * @returns {boolean}
554
+ */
555
+ const isKnownNetworkDomain = (domain) => {
556
+ const normalizedDomain = domain.toLowerCase();
557
+ if (KNOWN_NETWORK_EXACT_DOMAINS.has(normalizedDomain)) return true;
558
+
559
+ const rootDomain = getStrictRegistrableRootDomain(normalizedDomain);
560
+ if (rootDomain && rootDomain !== normalizedDomain && KNOWN_NETWORK_SUBDOMAIN_ROOTS.has(rootDomain)) {
561
+ return true;
562
+ }
563
+
564
+ // Fallback para sufixos fora da lista de TLDs (ex.: goo.gl), sem varrer array inteiro.
565
+ let dotIndex = normalizedDomain.indexOf('.');
566
+ while (dotIndex !== -1) {
567
+ const parentDomain = normalizedDomain.slice(dotIndex + 1);
568
+ if (KNOWN_NETWORK_EXACT_DOMAINS.has(parentDomain)) return true;
569
+ dotIndex = normalizedDomain.indexOf('.', dotIndex + 1);
570
+ }
571
+
572
+ return false;
573
+ };
574
+
575
+ /**
576
+ * Busca o primeiro indicativo de URL no token.
577
+ * @param {string} token
578
+ * @returns {number}
579
+ */
580
+ const findUrlHintIndex = (token) => {
581
+ let firstIndex = -1;
582
+
583
+ for (const hint of URL_HINTS) {
584
+ const index = token.indexOf(hint);
585
+ if (index !== -1 && (firstIndex === -1 || index < firstIndex)) {
586
+ firstIndex = index;
587
+ }
588
+ }
589
+
590
+ return firstIndex;
591
+ };
592
+
593
+ /**
594
+ * Extrai host de token com http(s)/www usando URL nativo.
595
+ * @param {string} token
596
+ * @returns {string | null}
597
+ */
598
+ const extractDomainFromUrlToken = (token) => {
599
+ const lowerToken = token.toLowerCase();
600
+ const hintIndex = findUrlHintIndex(lowerToken);
601
+ if (hintIndex === -1) return null;
602
+ if (hintIndex > 0) {
603
+ const previousChar = lowerToken[hintIndex - 1];
604
+ if (previousChar === '@' || isAsciiLetter(previousChar) || isDigit(previousChar)) {
605
+ return null;
606
+ }
607
+ }
608
+
609
+ let urlCandidate = token.slice(hintIndex);
610
+ const normalizedCandidate = urlCandidate.toLowerCase();
611
+ if (!normalizedCandidate.startsWith('http://') && !normalizedCandidate.startsWith('https://')) {
612
+ urlCandidate = `https://${urlCandidate}`;
613
+ }
614
+
615
+ try {
616
+ const parsedUrl = new URL(urlCandidate);
617
+ const host = normalizeHost(parsedUrl.hostname);
618
+ if (!isValidDomainStructure(host)) return null;
619
+ if (!hasAnyKnownTldSuffix(host) && !isKnownNetworkDomain(host)) return null;
620
+ return host;
621
+ } catch {
622
+ return null;
623
+ }
624
+ };
625
+
626
+ /**
627
+ * Extrai host de token sem protocolo, validando domínio manualmente.
628
+ * @param {string} token
629
+ * @returns {string | null}
630
+ */
631
+ const extractDomainFromPlainToken = (token) => {
632
+ const lowerToken = token.toLowerCase();
633
+ if (lowerToken.includes('@')) return null;
634
+
635
+ let host = '';
636
+ for (const char of lowerToken) {
637
+ if (HOST_TERMINATORS.has(char)) break;
638
+ host += char;
639
+ }
640
+
641
+ host = normalizeHost(host);
642
+ if (!isValidDomainStructure(host)) return null;
643
+ if (!hasStrictKnownTldSuffix(host) && !isKnownNetworkDomain(host)) return null;
644
+
645
+ return host;
646
+ };
647
+
648
+ /**
649
+ * Extrai domínios válidos de um texto sem uso de regex.
650
+ * @param {string} text
651
+ * @returns {string[]}
652
+ */
653
+ export const extractDomainsNoRegex = (text) => {
654
+ const tokens = tokenizeText(text);
655
+ if (tokens.length === 0) return [];
656
+ const domains = new Set();
657
+
658
+ for (const token of tokens) {
659
+ const partialTokens = splitCompositeToken(token);
660
+ for (const partialToken of partialTokens) {
661
+ const cleanedToken = stripEdgePunctuation(partialToken);
662
+ if (!cleanedToken) continue;
663
+
664
+ const urlDomain = extractDomainFromUrlToken(cleanedToken);
665
+ if (urlDomain) {
666
+ domains.add(urlDomain);
667
+ continue;
668
+ }
669
+
670
+ const plainDomain = extractDomainFromPlainToken(cleanedToken);
671
+ if (plainDomain) domains.add(plainDomain);
672
+ }
673
+ }
674
+
675
+ return Array.from(domains);
676
+ };
677
+
678
+ /**
679
+ * Normaliza e remove duplicados da allowlist.
680
+ * @param {string[]} allowedDomains
681
+ * @returns {string[]}
682
+ */
683
+ const normalizeAllowedDomains = (allowedDomains = []) => {
684
+ const normalizedDomains = new Set();
685
+
686
+ for (const allowedDomain of allowedDomains) {
687
+ const normalizedDomain = normalizeHost(String(allowedDomain || ''));
688
+ if (normalizedDomain) normalizedDomains.add(normalizedDomain);
689
+ }
690
+
691
+ return Array.from(normalizedDomains);
692
+ };
693
+
694
+ /**
695
+ * Aceita o domínio exato ou subdomínios de um permitido já normalizado.
696
+ * @param {string} domain
697
+ * @param {string[]} normalizedAllowedDomains
698
+ * @returns {boolean}
699
+ */
700
+ const isDomainAllowed = (domain, normalizedAllowedDomains) =>
701
+ normalizedAllowedDomains.some((allowedDomain) => domain === allowedDomain || domain.endsWith(`.${allowedDomain}`));
702
+
703
+ /**
704
+ * Monta a lista final de domínios permitidos (redes conhecidas + personalizados).
705
+ * @param {string[]} allowedNetworks
706
+ * @param {string[]} allowedCustomDomains
707
+ * @returns {string[]}
708
+ */
709
+ const getAllowedDomains = (allowedNetworks = [], allowedCustomDomains = []) => {
710
+ const domains = [];
711
+ for (const network of allowedNetworks) {
712
+ if (KNOWN_NETWORKS[network]) {
713
+ domains.push(...KNOWN_NETWORKS[network]);
714
+ }
715
+ }
716
+ return [...domains, ...allowedCustomDomains];
717
+ };
718
+
719
+ /**
720
+ * Retorna true quando existir um link que não esteja na lista permitida.
721
+ * @param {string} text
722
+ * @param {string[]} normalizedAllowedDomains
723
+ * @returns {boolean}
724
+ */
725
+ export const isLinkDetected = (text, normalizedAllowedDomains = []) => {
726
+ const domains = extractDomainsNoRegex(text);
727
+ if (domains.length === 0) return false;
728
+ if (normalizedAllowedDomains.length === 0) return true;
729
+ return domains.some((domain) => !isDomainAllowed(domain, normalizedAllowedDomains));
730
+ };
731
+
732
+ /**
733
+ * Aplica a regra de antilink do grupo. Retorna true quando removeu e deve pular o restante.
734
+ * @param {Object} params
735
+ * @param {import('@whiskeysockets/baileys').WASocket} params.sock
736
+ * @param {Object} params.messageInfo
737
+ * @param {string} params.extractedText
738
+ * @param {string} params.remoteJid
739
+ * @param {string} params.senderJid
740
+ * @param {string} params.botJid
741
+ * @returns {Promise<boolean>}
742
+ */
743
+ export const handleAntiLink = async ({ sock, messageInfo, extractedText, remoteJid, senderJid, botJid }) => {
744
+ if (!senderJid) return false;
745
+ if (senderJid === botJid) return false;
746
+ const groupConfig = await groupConfigStore.getGroupConfig(remoteJid);
747
+ if (!groupConfig || !groupConfig.antilinkEnabled) return false;
748
+
749
+ const allowedDomains = getAllowedDomains(groupConfig.antilinkAllowedNetworks || [], groupConfig.antilinkAllowedDomains || []);
750
+ const normalizedAllowedDomains = normalizeAllowedDomains(allowedDomains);
751
+ if (!isLinkDetected(extractedText, normalizedAllowedDomains)) return false;
752
+
753
+ const isAdmin = await isUserAdmin(remoteJid, senderJid);
754
+ const senderIsBot = senderJid === botJid;
755
+
756
+ if (!isAdmin && !senderIsBot) {
757
+ try {
758
+ await updateGroupParticipants(sock, remoteJid, [senderJid], 'remove');
759
+ const senderUser = getJidUser(senderJid);
760
+ await sendAndStore(sock, remoteJid, {
761
+ text: `🚫 @${senderUser || 'usuario'} foi removido por enviar um link.`,
762
+ mentions: senderUser ? [senderJid] : [],
763
+ });
764
+ await sendAndStore(sock, remoteJid, { delete: messageInfo.key });
765
+
766
+ logger.info(`Usuário ${senderJid} removido do grupo ${remoteJid} por enviar link.`, {
767
+ action: 'antilink_remove',
768
+ groupId: remoteJid,
769
+ userId: senderJid,
770
+ });
771
+
772
+ return true;
773
+ } catch (error) {
774
+ logger.error(`Falha ao remover usuário com antilink: ${error.message}`, {
775
+ action: 'antilink_error',
776
+ groupId: remoteJid,
777
+ userId: senderJid,
778
+ error: error.stack,
779
+ });
780
+ }
781
+ } else if (isAdmin && !senderIsBot) {
782
+ try {
783
+ const senderUser = getJidUser(senderJid);
784
+ await sendAndStore(sock, remoteJid, {
785
+ text: `ⓘ @${senderUser || 'admin'} (admin) enviou um link.`,
786
+ mentions: senderUser ? [senderJid] : [],
787
+ });
788
+ logger.info(`Admin ${senderJid} enviou um link no grupo ${remoteJid} (aviso enviado).`, {
789
+ action: 'antilink_admin_link_detected',
790
+ groupId: remoteJid,
791
+ userId: senderJid,
792
+ });
793
+ } catch (error) {
794
+ logger.error(`Falha ao enviar aviso de link de admin: ${error.message}`, {
795
+ action: 'antilink_admin_warning_error',
796
+ groupId: remoteJid,
797
+ userId: senderJid,
798
+ error: error.stack,
799
+ });
800
+ }
801
+ }
802
+
803
+ return false;
804
+ };