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 +13 -0
- package/lib/ForumSocket.js +456 -0
- package/lib/parser.js +44 -0
- package/package.json +16 -0
package/index.js
ADDED
|
@@ -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
|
+
}
|