mush-forum-socket 1.0.0

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/index.js ADDED
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ const ForumSocket = require('./lib/ForumSocket');
4
+ const {
5
+ extractReportData,
6
+ formatReport
7
+ } = require('./lib/parser');
8
+
9
+ module.exports = {
10
+ ForumSocket,
11
+ extractReportData,
12
+ formatReport
13
+ };
@@ -0,0 +1,456 @@
1
+ 'use strict';
2
+
3
+ const EventEmitter = require('events');
4
+ const axios = require('axios');
5
+
6
+ const {
7
+ formatReport
8
+ } = require('./parser');
9
+
10
+ class ForumSocket extends EventEmitter {
11
+ constructor(options = {}) {
12
+ super();
13
+
14
+ if (typeof options.getAuth !== 'function') {
15
+ throw new Error('[ForumSocket] A opção `getAuth` é obrigatória.');
16
+ }
17
+
18
+ this.forumUrl = options.forumUrl ?? null;
19
+ this.scraperUrl = options.scraperUrl ?? null;
20
+ this.proxyUrl = options.proxyUrl ?? null;
21
+ this.getAuth = options.getAuth;
22
+ this.categories = Array.isArray(options.categories) ? options.categories : [];
23
+ this.reportsCategory = options.reportsCategory ?? null;
24
+ this.reconnectDelay = options.reconnectDelay ?? 5000;
25
+ this.timeout = options.timeout ?? 30000;
26
+
27
+ this.pollCounter = 0;
28
+ this.active = false;
29
+ this.sid = null;
30
+ this.cfSession = null;
31
+ this.cfSessionExpiry = 0;
32
+ this.pollErrors = 0;
33
+ this.reconnectTimeout = null;
34
+ }
35
+
36
+ async start() {
37
+ this.stop();
38
+
39
+ this.active = true;
40
+
41
+ await this._connect();
42
+ }
43
+
44
+ stop() {
45
+ this.active = false;
46
+
47
+ if (this.reconnectTimeout) {
48
+ clearTimeout(this.reconnectTimeout);
49
+
50
+ this.reconnectTimeout = null;
51
+ }
52
+
53
+ if (this.sid) {
54
+ this.emit('disconnect', {
55
+ reason: 'manual_stop'
56
+ });
57
+
58
+ this.sid = null;
59
+ }
60
+ }
61
+
62
+ connect() {
63
+ return this.start();
64
+ }
65
+
66
+ disconnect() {
67
+ return this.stop();
68
+ }
69
+
70
+ async enterRoom(room) {
71
+ if (!this.active || !this.sid || !this.headers || !this.csrfToken || !this.forumUrl) {
72
+ throw new Error('[ForumSocket] Cliente não conectado.');
73
+ }
74
+
75
+ const roomUrl = `${this.forumUrl}/socket.io/?EIO=4&transport=polling&t=${this._generateT()}&sid=${this.sid}&_csrf=${this.csrfToken}`;
76
+
77
+ await this._proxyRequest(roomUrl, 'POST', {
78
+ ...this.headers,
79
+ 'Content-Type': 'text/plain;charset=UTF-8'
80
+ }, `42["meta.rooms.enter",{"enter":"${room}"}]`);
81
+ }
82
+
83
+ async _proxyRequest(url, method, headers, body, options = {}) {
84
+ if (!this.proxyUrl) {
85
+ throw new Error('[ForumSocket] `proxyUrl` não configurada.');
86
+ }
87
+
88
+ const response = await axios.post(this.proxyUrl, {
89
+ url,
90
+ method,
91
+ headers,
92
+ body
93
+ }, {
94
+ timeout: this.timeout,
95
+ ...options
96
+ });
97
+
98
+ return {
99
+ status: response.status,
100
+ body: response.data.data,
101
+ headers: response.data.headers
102
+ };
103
+ }
104
+
105
+ async _getCfClearance() {
106
+ if (this.cfSession && Date.now() < this.cfSessionExpiry) {
107
+ return this.cfSession;
108
+ }
109
+
110
+ if (!this.scraperUrl || !this.forumUrl) {
111
+ throw new Error('[ForumSocket] `scraperUrl` ou `forumUrl` não configurada.');
112
+ }
113
+
114
+ try {
115
+ const response = await axios.post(this.scraperUrl, {
116
+ url: this.forumUrl,
117
+ mode: 'waf-session'
118
+ }, {
119
+ timeout: 120000
120
+ });
121
+
122
+ const session = response.data;
123
+
124
+ if (!session || session.code !== 200) return null;
125
+
126
+ this.cfSession = session;
127
+ this.cfSessionExpiry = Date.now() + (30 * 60 * 1000);
128
+
129
+ return session;
130
+ } catch (e) {
131
+ this.emit('error', new Error(`Falha ao obter cf_clearance: ${e.message}`));
132
+
133
+ return null;
134
+ }
135
+ }
136
+
137
+ _buildHeaders(userAuth, cfSession) {
138
+ const combinedCookies = [userAuth.cookies.filter(c => c.name !== 'cf_clearance').map(c => `${c.name}=${c.value}`).join('; '), (cfSession?.cookies?.length ? cfSession.cookies.filter(c => c.name === 'cf_clearance').map(c => `${c.name}=${c.value}`).join('; ') : '')].filter(Boolean).join('; ');
139
+
140
+ const userAgent = cfSession.headers?.['user-agent'] ?? userAuth.user?.agent;
141
+
142
+ return {
143
+ 'Cookie': combinedCookies,
144
+ 'User-Agent': userAgent,
145
+ 'Origin': this.forumUrl,
146
+ 'Referer': `${this.forumUrl}/`
147
+ };
148
+ }
149
+
150
+ _updateCookies(res) {
151
+ if (!res?.headers?.['set-cookie'] || !this.headers) return;
152
+
153
+ const setCookie = res.headers['set-cookie'];
154
+ const cookieArray = Array.isArray(setCookie) ? setCookie : [setCookie];
155
+
156
+ for (const str of cookieArray) {
157
+ const match = str.match(/^([^=]+)=([^;]+)/);
158
+
159
+ if (match) {
160
+ const [, name, value] = match.map(s => s.trim());
161
+
162
+ const regex = new RegExp(`${name}=[^;]+`);
163
+
164
+ this.headers['Cookie'] = regex.test(this.headers['Cookie']) ? this.headers['Cookie'].replace(regex, `${name}=${value}`) : `${this.headers['Cookie']}; ${name}=${value}`;
165
+ }
166
+ }
167
+ }
168
+
169
+ _generateT() {
170
+ return Date.now().toString(36) + (this.pollCounter++).toString(36);
171
+ }
172
+
173
+ _parseEngineIOPackets(raw) {
174
+ if (!raw || typeof raw !== 'string') return [];
175
+
176
+ const packets = [];
177
+
178
+ let i = 0;
179
+
180
+ while (i < raw.length) {
181
+ const type = raw[i];
182
+
183
+ if (type === '0') {
184
+ let depth = 0;
185
+
186
+ let j = i + 1;
187
+
188
+ while (j < raw.length) {
189
+ if (raw[j] === '{') depth++;
190
+ if (raw[j] === '}') depth--;
191
+
192
+ if (depth === 0) break;
193
+
194
+ j++;
195
+ }
196
+
197
+ packets.push({
198
+ type: 'open',
199
+ data: raw.substring(i + 1, j + 1)
200
+ });
201
+
202
+ i = j + 1;
203
+ } else if (type === '2' || type === '3' || type === '6') {
204
+ packets.push({
205
+ type: type === '2' ? 'ping' : type === '3' ? 'pong' : 'noop'
206
+ });
207
+
208
+ i++;
209
+ } else if (type === '4') {
210
+ const sioType = raw[i + 1];
211
+
212
+ if (sioType === '0' || sioType === '2') {
213
+ const openChar = sioType === '0' ? '{' : '[';
214
+
215
+ const closeChar = sioType === '0' ? '}' : ']';
216
+
217
+ if (raw[i + 2] === openChar) {
218
+ let depth = 0;
219
+
220
+ let j = i + 2;
221
+
222
+ while (j < raw.length) {
223
+ if (raw[j] === openChar) depth++;
224
+ if (raw[j] === closeChar) depth--;
225
+
226
+ if (depth === 0) break;
227
+
228
+ j++;
229
+ }
230
+
231
+ packets.push({
232
+ type: 'message',
233
+ data: raw.substring(i + 1, j + 1)
234
+ });
235
+
236
+ i = j + 1;
237
+ } else {
238
+ packets.push({
239
+ type: 'message',
240
+ data: sioType === '0' ? '0' : raw.substring(i + 1)
241
+ });
242
+
243
+ i += (sioType === '0' ? 2 : raw.length);
244
+ }
245
+ } else {
246
+ packets.push({
247
+ type: 'message',
248
+ data: raw.substring(i + 1)
249
+ });
250
+
251
+ break;
252
+ }
253
+ } else {
254
+ break;
255
+ }
256
+ }
257
+
258
+ return packets;
259
+ }
260
+
261
+ _parseSocketIOEvent(messageData) {
262
+ if (!messageData?.length) return null;
263
+
264
+ const sioType = messageData[0];
265
+
266
+ if (sioType === '2') {
267
+ try {
268
+ const jsonData = JSON.parse(messageData.substring(1));
269
+
270
+ return Array.isArray(jsonData) && jsonData.length >= 1 ? {
271
+ event: jsonData[0],
272
+ data: jsonData[1] ?? null
273
+ } : null;
274
+ } catch {
275
+ return null;
276
+ }
277
+ }
278
+
279
+ return sioType === '0' ? {
280
+ event: '__connect',
281
+ data: messageData.substring(1)
282
+ } : null;
283
+ }
284
+
285
+ async _connect() {
286
+ if (!this.active) return;
287
+
288
+ try {
289
+ const userAuth = await this.getAuth();
290
+
291
+ if (!userAuth?.cookies?.length || !userAuth?.csrf?.token) {
292
+ throw new Error('Credenciais de autenticação ausentes.');
293
+ }
294
+
295
+ this.csrfToken = userAuth.csrf.token;
296
+
297
+ const cfSession = await this._getCfClearance();
298
+
299
+ if (!cfSession) {
300
+ throw new Error('Falha ao obter sessão do Cloudflare WAF.');
301
+ }
302
+
303
+ this.headers = this._buildHeaders(userAuth, cfSession);
304
+
305
+ const handshakeUrl = `${this.forumUrl}/socket.io/?EIO=4&transport=polling&t=${this._generateT()}&_csrf=${this.csrfToken}`;
306
+
307
+ const handshakeRes = await this._proxyRequest(handshakeUrl, 'GET', this.headers);
308
+
309
+ this._updateCookies(handshakeRes);
310
+
311
+ const handshakeBody = typeof handshakeRes.body === 'string' ? handshakeRes.body : JSON.stringify(handshakeRes.body);
312
+
313
+ const packets = this._parseEngineIOPackets(handshakeBody);
314
+
315
+ const openPacket = packets.find(p => p.type === 'open');
316
+
317
+ if (!openPacket) {
318
+ throw new Error(`Handshake falhou: ${handshakeBody?.substring(0, 150)}`);
319
+ }
320
+
321
+ const sessionData = JSON.parse(openPacket.data);
322
+
323
+ this.sid = sessionData.sid;
324
+
325
+ const connectPostUrl = `${this.forumUrl}/socket.io/?EIO=4&transport=polling&t=${this._generateT()}&sid=${this.sid}&_csrf=${this.csrfToken}`;
326
+
327
+ const connectPostRes = await this._proxyRequest(connectPostUrl, 'POST', {
328
+ ...this.headers,
329
+ 'Content-Type': 'text/plain;charset=UTF-8'
330
+ }, '40');
331
+
332
+ this._updateCookies(connectPostRes);
333
+
334
+ const connectGetUrl = `${this.forumUrl}/socket.io/?EIO=4&transport=polling&t=${this._generateT()}&sid=${this.sid}&_csrf=${this.csrfToken}`;
335
+
336
+ const connectRes = await this._proxyRequest(connectGetUrl, 'GET', this.headers);
337
+
338
+ this._updateCookies(connectRes);
339
+
340
+ for (const cid of this.categories) {
341
+ await this.enterRoom(`category_${cid}`);
342
+ }
343
+
344
+ this.pollErrors = 0;
345
+
346
+ this.emit('connect', {
347
+ sid: this.sid
348
+ });
349
+
350
+ this._poll();
351
+ } catch (e) {
352
+ this.emit('error', e);
353
+
354
+ this._scheduleReconnect(this.reconnectDelay);
355
+ }
356
+ }
357
+
358
+ async _poll() {
359
+ if (!this.active || !this.sid) return;
360
+
361
+ try {
362
+ const pollUrl = `${this.forumUrl}/socket.io/?EIO=4&transport=polling&t=${this._generateT()}&sid=${this.sid}&_csrf=${this.csrfToken}`;
363
+
364
+ const res = await this._proxyRequest(pollUrl, 'GET', this.headers);
365
+
366
+ this._updateCookies(res);
367
+
368
+ const body = typeof res.body === 'string' ? res.body : JSON.stringify(res.body);
369
+
370
+ this.pollErrors = 0;
371
+
372
+ const packets = this._parseEngineIOPackets(body);
373
+
374
+ for (const packet of packets) {
375
+ if (packet.type === 'ping') {
376
+ const pongUrl = `${this.forumUrl}/socket.io/?EIO=4&transport=polling&t=${this._generateT()}&sid=${this.sid}&_csrf=${this.csrfToken}`;
377
+
378
+ await this._proxyRequest(pongUrl, 'POST', {
379
+ ...this.headers,
380
+ 'Content-Type': 'text/plain;charset=UTF-8'
381
+ }, '3');
382
+ } else if (packet.type === 'message') {
383
+ const parsed = this._parseSocketIOEvent(packet.data);
384
+
385
+ if (parsed && parsed.event !== '__connect') {
386
+ this._handleEvent(parsed.event, parsed.data);
387
+ }
388
+ }
389
+ }
390
+
391
+ if (this.active) {
392
+ setTimeout(() => this._poll(), 100);
393
+ }
394
+ } catch (e) {
395
+ const status = e.response?.status;
396
+
397
+ this.emit('error', new Error(`Erro no polling: ${status ?? e.message}`));
398
+
399
+ if (status === 400 || (status >= 500 && status < 600)) {
400
+ this.cfSession = null;
401
+ this.cfSessionExpiry = 0;
402
+ this.sid = null;
403
+
404
+ this._scheduleReconnect(this.reconnectDelay);
405
+ } else {
406
+ this.pollErrors++;
407
+
408
+ if (this.pollErrors >= 5) {
409
+ this.cfSession = null;
410
+ this.cfSessionExpiry = 0;
411
+ this.sid = null;
412
+
413
+ this._scheduleReconnect(this.reconnectDelay * 2);
414
+ } else {
415
+ setTimeout(() => this._poll(), 3000);
416
+ }
417
+ }
418
+ }
419
+ }
420
+
421
+ _handleEvent(eventName, data) {
422
+ this.emit('event', {
423
+ event: eventName,
424
+ data
425
+ });
426
+
427
+ if (eventName === 'event:new_post') {
428
+ const post = data?.posts?.[0];
429
+
430
+ if (!post) return;
431
+
432
+ this.emit('new_post', post);
433
+
434
+ const cid = post.topic?.cid ?? post.category?.cid;
435
+
436
+ if (cid === this.reportsCategory && post.isMainPost) {
437
+ this.emit('report', formatReport(post, this.forumUrl));
438
+ }
439
+ }
440
+ }
441
+
442
+ _scheduleReconnect(delay) {
443
+ if (!this.active) return;
444
+
445
+ if (this.reconnectTimeout) clearTimeout(this.reconnectTimeout);
446
+
447
+ this.emit('disconnect', {
448
+ reason: 'reconnecting',
449
+ delay
450
+ });
451
+
452
+ this.reconnectTimeout = setTimeout(() => this._connect(), delay);
453
+ }
454
+ }
455
+
456
+ module.exports = ForumSocket;
package/lib/parser.js ADDED
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ const extractReportData = (content = '') => ({
4
+ suspects: content.match(/infrator.*?:\s*(?:<[^>]+>\s*)*R:\s*([^<\n]+)/i)?.[1]?.trim() ?? null,
5
+ reason: content.match(/motivo.*?:\s*(?:<[^>]+>\s*)*R:\s*([^<\n]+)/i)?.[1]?.trim() ?? null,
6
+ incidentDate: content.match(/data.*?:\s*(?:<[^>]+>\s*)*R:\s*([^<\n]+)/i)?.[1]?.trim() ?? null,
7
+ proofUrl: content.match(/<img[^>]+src=["']([^"']+)["']/i)?.[1] ?? content.match(/https?:\/\/[^\s<"']+/i)?.[0] ?? null
8
+ });
9
+
10
+ const formatReport = (post, forumUrl = null) => {
11
+ if (!post) return null;
12
+
13
+ const extracted = extractReportData(post.content ?? '');
14
+
15
+ return {
16
+ tid: post.tid ?? null,
17
+ pid: post.pid ?? null,
18
+ title: post.topic?.title ?? null,
19
+ topicUrl: post.tid && forumUrl ? `${forumUrl}/topic/${post.tid}` : null,
20
+ suspects: extracted.suspects,
21
+ reason: extracted.reason,
22
+ incidentDate: extracted.incidentDate,
23
+ proofUrl: extracted.proofUrl,
24
+ author: {
25
+ uid: post.user?.uid ?? null,
26
+ username: post.user?.username ?? null,
27
+ userslug: post.user?.userslug ?? null,
28
+ picture: post.user?.picture ?? null,
29
+ profileUrl: post.user?.userslug && forumUrl ? `${forumUrl}/user/${post.user.userslug}` : null
30
+ },
31
+ category: {
32
+ cid: post.category?.cid ?? post.topic?.cid ?? null,
33
+ name: post.category?.name ?? null
34
+ },
35
+ timestamp: post.timestamp ?? Date.now(),
36
+ timestampUnix: Math.floor((post.timestamp ?? Date.now()) / 1000),
37
+ raw: post
38
+ };
39
+ };
40
+
41
+ module.exports = {
42
+ extractReportData,
43
+ formatReport
44
+ };
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "mush-forum-socket",
3
+ "version": "1.0.0",
4
+ "main": "index.js",
5
+ "directories": {
6
+ "lib": "lib"
7
+ },
8
+ "keywords": [],
9
+ "author": "sophiwa",
10
+ "license": "MIT",
11
+ "description": "",
12
+ "dependencies": {
13
+ "axios": "^1.20.0",
14
+ "events": "^3.3.0"
15
+ }
16
+ }