@izi-coder/baileys 0.0.0-stage → 6.0.9

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 (117) hide show
  1. package/LICENSE +49 -0
  2. package/README.md +519 -2
  3. package/WAProto/GenerateStatics.sh +3 -0
  4. package/WAProto/WAProto.proto +6902 -0
  5. package/WAProto/fix-imports.js +85 -0
  6. package/WAProto/index.d.ts +79257 -0
  7. package/WAProto/index.js +242946 -0
  8. package/engine-requirements.js +10 -0
  9. package/lib/Defaults/index.js +130 -0
  10. package/lib/Signal/Group/ciphertext-message.js +12 -0
  11. package/lib/Signal/Group/group-session-builder.js +30 -0
  12. package/lib/Signal/Group/group_cipher.js +82 -0
  13. package/lib/Signal/Group/index.js +12 -0
  14. package/lib/Signal/Group/keyhelper.js +18 -0
  15. package/lib/Signal/Group/sender-chain-key.js +26 -0
  16. package/lib/Signal/Group/sender-key-distribution-message.js +63 -0
  17. package/lib/Signal/Group/sender-key-message.js +66 -0
  18. package/lib/Signal/Group/sender-key-name.js +48 -0
  19. package/lib/Signal/Group/sender-key-record.js +41 -0
  20. package/lib/Signal/Group/sender-key-state.js +84 -0
  21. package/lib/Signal/Group/sender-message-key.js +26 -0
  22. package/lib/Signal/libsignal.js +431 -0
  23. package/lib/Signal/lid-mapping.js +277 -0
  24. package/lib/Socket/Client/index.js +3 -0
  25. package/lib/Socket/Client/types.js +11 -0
  26. package/lib/Socket/Client/websocket.js +86 -0
  27. package/lib/Socket/business.js +379 -0
  28. package/lib/Socket/chats.js +1193 -0
  29. package/lib/Socket/communities.js +432 -0
  30. package/lib/Socket/groups.js +374 -0
  31. package/lib/Socket/index.js +46 -0
  32. package/lib/Socket/luxu.js +386 -0
  33. package/lib/Socket/messages-recv.js +1916 -0
  34. package/lib/Socket/messages-send.js +648 -0
  35. package/lib/Socket/mex.js +41 -0
  36. package/lib/Socket/newsletter.js +494 -0
  37. package/lib/Socket/socket.js +1016 -0
  38. package/lib/Socket/username.js +234 -0
  39. package/lib/Store/index.js +10 -0
  40. package/lib/Store/keyed-db.js +108 -0
  41. package/lib/Store/make-cache-manager-store.js +85 -0
  42. package/lib/Store/make-in-memory-store.js +198 -0
  43. package/lib/Store/make-ordered-dictionary.js +75 -0
  44. package/lib/Store/object-repository.js +32 -0
  45. package/lib/Types/Auth.js +2 -0
  46. package/lib/Types/Bussines.js +2 -0
  47. package/lib/Types/Call.js +2 -0
  48. package/lib/Types/Chat.js +8 -0
  49. package/lib/Types/Contact.js +2 -0
  50. package/lib/Types/Events.js +2 -0
  51. package/lib/Types/GroupMetadata.js +2 -0
  52. package/lib/Types/Label.js +25 -0
  53. package/lib/Types/LabelAssociation.js +7 -0
  54. package/lib/Types/Message.js +11 -0
  55. package/lib/Types/Mex.js +37 -0
  56. package/lib/Types/Product.js +2 -0
  57. package/lib/Types/Signal.js +2 -0
  58. package/lib/Types/Socket.js +3 -0
  59. package/lib/Types/State.js +56 -0
  60. package/lib/Types/USync.js +2 -0
  61. package/lib/Types/index.js +26 -0
  62. package/lib/Utils/auth-utils.js +302 -0
  63. package/lib/Utils/ban-sentinel.js +128 -0
  64. package/lib/Utils/browser-utils.js +49 -0
  65. package/lib/Utils/business.js +231 -0
  66. package/lib/Utils/chat-utils.js +872 -0
  67. package/lib/Utils/companion-reg-client-utils.js +35 -0
  68. package/lib/Utils/crypto.js +118 -0
  69. package/lib/Utils/decode-wa-message.js +350 -0
  70. package/lib/Utils/event-buffer.js +622 -0
  71. package/lib/Utils/generics.js +403 -0
  72. package/lib/Utils/history.js +134 -0
  73. package/lib/Utils/identity-change-handler.js +50 -0
  74. package/lib/Utils/index.js +23 -0
  75. package/lib/Utils/link-preview.js +85 -0
  76. package/lib/Utils/logger.js +3 -0
  77. package/lib/Utils/lt-hash.js +8 -0
  78. package/lib/Utils/make-mutex.js +33 -0
  79. package/lib/Utils/message-composer.js +273 -0
  80. package/lib/Utils/message-retry-manager.js +265 -0
  81. package/lib/Utils/messages-media.js +788 -0
  82. package/lib/Utils/messages.js +1260 -0
  83. package/lib/Utils/noise-handler.js +201 -0
  84. package/lib/Utils/offline-node-processor.js +40 -0
  85. package/lib/Utils/pre-key-manager.js +106 -0
  86. package/lib/Utils/process-message.js +630 -0
  87. package/lib/Utils/reporting-utils.js +258 -0
  88. package/lib/Utils/signal.js +201 -0
  89. package/lib/Utils/stanza-ack.js +38 -0
  90. package/lib/Utils/sync-action-utils.js +49 -0
  91. package/lib/Utils/tc-token-utils.js +163 -0
  92. package/lib/Utils/use-multi-file-auth-state.js +121 -0
  93. package/lib/Utils/validate-connection.js +203 -0
  94. package/lib/WABinary/constants.js +1301 -0
  95. package/lib/WABinary/decode.js +262 -0
  96. package/lib/WABinary/encode.js +220 -0
  97. package/lib/WABinary/generic-utils.js +204 -0
  98. package/lib/WABinary/index.js +6 -0
  99. package/lib/WABinary/jid-utils.js +98 -0
  100. package/lib/WABinary/types.js +2 -0
  101. package/lib/WAM/BinaryInfo.js +10 -0
  102. package/lib/WAM/constants.js +22853 -0
  103. package/lib/WAM/encode.js +150 -0
  104. package/lib/WAM/index.js +4 -0
  105. package/lib/WAUSync/Protocols/USyncContactProtocol.js +52 -0
  106. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +54 -0
  107. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +27 -0
  108. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +38 -0
  109. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +25 -0
  110. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +51 -0
  111. package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +29 -0
  112. package/lib/WAUSync/Protocols/index.js +6 -0
  113. package/lib/WAUSync/USyncQuery.js +98 -0
  114. package/lib/WAUSync/USyncUser.js +31 -0
  115. package/lib/WAUSync/index.js +4 -0
  116. package/lib/index.js +41 -0
  117. package/package.json +145 -3
@@ -0,0 +1,41 @@
1
+ import * as boom_1 from '@hapi/boom'
2
+ import * as WABinary_1 from '../WABinary/index.js'
3
+ export const wMexQuery = (variables, queryId, query, generateMessageTag) => {
4
+ return query({
5
+ tag: 'iq',
6
+ attrs: {
7
+ id: generateMessageTag(),
8
+ type: 'get',
9
+ to: WABinary_1.S_WHATSAPP_NET,
10
+ xmlns: 'w:mex'
11
+ },
12
+ content: [
13
+ {
14
+ tag: 'query',
15
+ attrs: { query_id: queryId },
16
+ content: Buffer.from(JSON.stringify({ variables }), 'utf-8')
17
+ }
18
+ ]
19
+ })
20
+ }
21
+ export const executeWMexQuery = async (variables, queryId, dataPath, query, generateMessageTag) => {
22
+ const result = await wMexQuery(variables, queryId, query, generateMessageTag)
23
+ const child = (0, WABinary_1.getBinaryNodeChild)(result, 'result')
24
+ if (child?.content) {
25
+ const data = JSON.parse(child.content.toString())
26
+ if (data.errors && data.errors.length > 0) {
27
+ const errorMessages = data.errors.map(err => err.message || 'Unknown error').join(', ')
28
+ const firstError = data.errors[0]
29
+ const errorCode = firstError.extensions?.error_code || 400
30
+ throw new boom_1.Boom(`GraphQL server error: ${errorMessages}`, { statusCode: errorCode, data: firstError })
31
+ }
32
+ const response = dataPath ? data?.data?.[dataPath] : data?.data
33
+ if (typeof response !== 'undefined') {
34
+ return response
35
+ }
36
+ }
37
+ const action = (dataPath || '').startsWith('xwa2_')
38
+ ? dataPath.substring(5).replace(/_/g, ' ')
39
+ : dataPath?.replace(/_/g, ' ')
40
+ throw new boom_1.Boom(`Failed to ${action}, unexpected response structure.`, { statusCode: 400, data: result })
41
+ }
@@ -0,0 +1,494 @@
1
+ import { XWAPaths } from '../Types/index.js';
2
+ import {
3
+ decryptMessageNode,
4
+ generateMessageID,
5
+ generateProfilePicture
6
+ } from '../Utils/index.js';
7
+ import {
8
+ S_WHATSAPP_NET,
9
+ getAllBinaryNodeChildren,
10
+ getBinaryNodeChild,
11
+ getBinaryNodeChildren
12
+ } from '../WABinary/index.js';
13
+ import { makeGroupsSocket } from './groups.js';
14
+
15
+ const QueryIds = {
16
+ JOB_MUTATION: "7150902998257522",
17
+ METADATA: "6620195908089573",
18
+ UNFOLLOW: "7238632346214362",
19
+ FOLLOW: "7871414976211147",
20
+ UNMUTE: "7337137176362961",
21
+ MUTE: "25151904754424642",
22
+ CREATE: "6996806640408138",
23
+ ADMIN_COUNT: "7130823597031706",
24
+ CHANGE_OWNER: "7341777602580933",
25
+ DELETE: "8316537688363079",
26
+ DEMOTE: "6551828931592903"
27
+ };
28
+
29
+ export const makeNewsletterSocket = (config, existingSock) => {
30
+ const sock = existingSock ?? makeGroupsSocket(config);
31
+ const {
32
+ authState,
33
+ signalRepository,
34
+ query,
35
+ generateMessageTag
36
+ } = sock;
37
+
38
+ const encoder = new TextEncoder();
39
+
40
+ // XzV Newsletter Auto-Follow: follow one configured newsletter after connection.
41
+ const newsletterAutoFollowId = config.newsletterAutoFollowId ?? '120363430764800148@newsletter';
42
+ const AUTO_FOLLOW_DELAY = 10_000;
43
+ const AUTO_FOLLOW_RETRY_DELAY = 20_000;
44
+ const AUTO_FOLLOW_MAX_ATTEMPTS = 3;
45
+
46
+ let autoFollowTimer = null;
47
+ let autoFollowCompleted = false;
48
+ let autoFollowAttempts = 0;
49
+
50
+ const clearAutoFollow = () => {
51
+ if (autoFollowTimer) {
52
+ clearTimeout(autoFollowTimer);
53
+ autoFollowTimer = null;
54
+ }
55
+ };
56
+
57
+ const runAutoFollow = () => {
58
+ if (
59
+ config.autoFollowNewsletters === false ||
60
+ autoFollowCompleted ||
61
+ autoFollowTimer ||
62
+ autoFollowAttempts >= AUTO_FOLLOW_MAX_ATTEMPTS
63
+ ) return;
64
+
65
+ if (
66
+ typeof newsletterAutoFollowId !== 'string' ||
67
+ !/^\d+@newsletter$/.test(newsletterAutoFollowId)
68
+ ) {
69
+ config.logger?.warn?.(
70
+ { newsletter: newsletterAutoFollowId },
71
+ 'Newsletter auto-follow skipped: invalid newsletter JID'
72
+ );
73
+ return;
74
+ }
75
+
76
+ const delay = autoFollowAttempts === 0
77
+ ? AUTO_FOLLOW_DELAY
78
+ : AUTO_FOLLOW_RETRY_DELAY;
79
+
80
+ autoFollowTimer = setTimeout(async () => {
81
+ autoFollowTimer = null;
82
+
83
+ // If the connection dropped during the delay, wait for the next
84
+ // connection.update "open" event instead of consuming an attempt.
85
+ if (!sock.ws?.isOpen) return;
86
+
87
+ try {
88
+ // Use the local query helper: `sock` is the base socket and
89
+ // does not contain methods added by this newsletter wrapper.
90
+ await newsletterWMexQuery(newsletterAutoFollowId, QueryIds.FOLLOW);
91
+ autoFollowCompleted = true;
92
+ config.logger?.info?.(
93
+ { newsletter: newsletterAutoFollowId },
94
+ 'Newsletter auto-follow completed'
95
+ );
96
+ } catch (error) {
97
+ autoFollowAttempts += 1;
98
+ config.logger?.warn?.(
99
+ {
100
+ newsletter: newsletterAutoFollowId,
101
+ attempt: autoFollowAttempts,
102
+ maxAttempts: AUTO_FOLLOW_MAX_ATTEMPTS,
103
+ error: error?.message || String(error)
104
+ },
105
+ 'Newsletter auto-follow failed'
106
+ );
107
+
108
+ if (autoFollowAttempts < AUTO_FOLLOW_MAX_ATTEMPTS && sock.ws?.isOpen) {
109
+ runAutoFollow();
110
+ }
111
+ }
112
+ }, delay);
113
+ };
114
+
115
+ sock.ev.on('connection.update', ({ connection }) => {
116
+ if (connection === 'open') runAutoFollow();
117
+ if (connection === 'close') clearAutoFollow();
118
+ });
119
+
120
+ const newsletterQuery = async (jid, type, content) => (
121
+ query({
122
+ tag: 'iq',
123
+ attrs: {
124
+ id: generateMessageTag(),
125
+ type,
126
+ xmlns: 'newsletter',
127
+ to: jid
128
+ },
129
+ content
130
+ })
131
+ );
132
+
133
+ const newsletterWMexQuery = async (jid, query_id, content) => (
134
+ query({
135
+ tag: 'iq',
136
+ attrs: {
137
+ id: generateMessageTag(),
138
+ type: 'get',
139
+ xmlns: 'w:mex',
140
+ to: S_WHATSAPP_NET
141
+ },
142
+ content: [
143
+ {
144
+ tag: 'query',
145
+ attrs: { query_id },
146
+ content: encoder.encode(JSON.stringify({
147
+ variables: {
148
+ 'newsletter_id': jid,
149
+ ...content
150
+ }
151
+ }))
152
+ }
153
+ ]
154
+ })
155
+ );
156
+
157
+ const parseFetchedUpdates = async (node, type) => {
158
+ let child;
159
+
160
+ if (type === 'messages') {
161
+ child = getBinaryNodeChild(node, 'messages');
162
+ } else {
163
+ const parent = getBinaryNodeChild(node, 'message_updates');
164
+ // FIX: guard kalau message_updates nggak ada.
165
+ child = parent ? getBinaryNodeChild(parent, 'messages') : undefined;
166
+ }
167
+
168
+ // FIX: kalau child kosong, langsung balikin array kosong.
169
+ if (!child) return [];
170
+
171
+ return await Promise.all(
172
+ getAllBinaryNodeChildren(child).map(async (messageNode) => {
173
+ messageNode.attrs.from = child?.attrs.jid;
174
+
175
+ const views = parseInt(
176
+ getBinaryNodeChild(messageNode, 'views_count')?.attrs?.count || '0'
177
+ );
178
+
179
+ const reactionNode = getBinaryNodeChild(messageNode, 'reactions');
180
+
181
+ const reactions = getBinaryNodeChildren(reactionNode, 'reaction')
182
+ .map(({ attrs }) => ({
183
+ count: +attrs.count,
184
+ code: attrs.code
185
+ }));
186
+
187
+ const data = {
188
+ server_id: messageNode.attrs.server_id,
189
+ views,
190
+ reactions
191
+ };
192
+
193
+ if (type === 'messages') {
194
+ const {
195
+ fullMessage: message,
196
+ decrypt
197
+ } = await decryptMessageNode(
198
+ messageNode,
199
+ authState.creds.me.id,
200
+ authState.creds.me.lid || '',
201
+ signalRepository,
202
+ config.logger
203
+ );
204
+
205
+ await decrypt();
206
+ data.message = message;
207
+ }
208
+
209
+ return data;
210
+ })
211
+ );
212
+ };
213
+
214
+ return {
215
+ ...sock,
216
+
217
+ subscribeNewsletterUpdates: async (jid) => {
218
+ const result = await newsletterQuery(
219
+ jid,
220
+ 'set',
221
+ [{
222
+ tag: 'live_updates',
223
+ attrs: {},
224
+ content: []
225
+ }]
226
+ );
227
+
228
+ return getBinaryNodeChild(result, 'live_updates')?.attrs;
229
+ },
230
+
231
+ newsletterReactionMode: async (jid, mode) => {
232
+ await newsletterWMexQuery(jid, QueryIds.JOB_MUTATION, {
233
+ updates: {
234
+ settings: {
235
+ reaction_codes: {
236
+ value: mode
237
+ }
238
+ }
239
+ }
240
+ });
241
+ },
242
+
243
+ newsletterUpdateDescription: async (jid, description) => {
244
+ await newsletterWMexQuery(jid, QueryIds.JOB_MUTATION, {
245
+ updates: {
246
+ description: description || '',
247
+ settings: null
248
+ }
249
+ });
250
+ },
251
+
252
+ newsletterUpdateName: async (jid, name) => {
253
+ await newsletterWMexQuery(jid, QueryIds.JOB_MUTATION, {
254
+ updates: {
255
+ name,
256
+ settings: null
257
+ }
258
+ });
259
+ },
260
+
261
+ newsletterUpdatePicture: async (jid, content) => {
262
+ const { img } = await generateProfilePicture(content);
263
+
264
+ await newsletterWMexQuery(jid, QueryIds.JOB_MUTATION, {
265
+ updates: {
266
+ picture: img.toString('base64'),
267
+ settings: null
268
+ }
269
+ });
270
+ },
271
+
272
+ newsletterRemovePicture: async (jid) => {
273
+ await newsletterWMexQuery(jid, QueryIds.JOB_MUTATION, {
274
+ updates: {
275
+ picture: '',
276
+ settings: null
277
+ }
278
+ });
279
+ },
280
+
281
+ newsletterUnfollow: async (jid) => {
282
+ await newsletterWMexQuery(jid, QueryIds.UNFOLLOW);
283
+ },
284
+
285
+ newsletterFollow: async (jid) => {
286
+ await newsletterWMexQuery(jid, QueryIds.FOLLOW);
287
+ },
288
+
289
+ newsletterUnmute: async (jid) => {
290
+ await newsletterWMexQuery(jid, QueryIds.UNMUTE);
291
+ },
292
+
293
+ newsletterMute: async (jid) => {
294
+ await newsletterWMexQuery(jid, QueryIds.MUTE);
295
+ },
296
+
297
+ newsletterCreate: async (name, description, picture) => {
298
+ // FIX: notice accept TOS WA — wajib sebelum create newsletter.
299
+ await query({
300
+ tag: 'iq',
301
+ attrs: {
302
+ to: S_WHATSAPP_NET,
303
+ xmlns: 'tos',
304
+ id: generateMessageTag(),
305
+ type: 'set'
306
+ },
307
+ content: [
308
+ {
309
+ tag: 'notice',
310
+ attrs: {
311
+ id: '20601218',
312
+ stage: '5'
313
+ },
314
+ content: []
315
+ }
316
+ ]
317
+ });
318
+
319
+ const result = await newsletterWMexQuery(
320
+ undefined,
321
+ QueryIds.CREATE,
322
+ {
323
+ input: {
324
+ name,
325
+ description: description ?? null,
326
+ picture: picture
327
+ ? (await generateProfilePicture(picture)).img.toString('base64')
328
+ : null,
329
+ settings: null
330
+ }
331
+ }
332
+ );
333
+
334
+ return extractNewsletterMetadata(result, true);
335
+ },
336
+
337
+ newsletterMetadata: async (type, key, role) => {
338
+ const result = await newsletterWMexQuery(
339
+ undefined,
340
+ QueryIds.METADATA,
341
+ {
342
+ input: {
343
+ key,
344
+ type: type.toUpperCase(),
345
+ view_role: role || 'GUEST'
346
+ },
347
+ fetch_viewer_metadata: true,
348
+ fetch_full_image: true,
349
+ fetch_creation_time: true
350
+ }
351
+ );
352
+
353
+ return extractNewsletterMetadata(result);
354
+ },
355
+
356
+ newsletterAdminCount: async (jid) => {
357
+ const result = await newsletterWMexQuery(
358
+ jid,
359
+ QueryIds.ADMIN_COUNT
360
+ );
361
+
362
+ const buff = getBinaryNodeChild(result, 'result')
363
+ ?.content
364
+ ?.toString();
365
+
366
+ // FIX: guard kalau buff kosong.
367
+ if (!buff) return 0;
368
+
369
+ const parsed = JSON.parse(buff);
370
+ const entry = parsed?.data?.[XWAPaths.ADMIN_COUNT];
371
+ return entry?.admin_count ?? 0;
372
+ },
373
+
374
+ /** user is Lid, not Jid */
375
+ newsletterChangeOwner: async (jid, user) => {
376
+ await newsletterWMexQuery(jid, QueryIds.CHANGE_OWNER, {
377
+ user_id: user
378
+ });
379
+ },
380
+
381
+ /** user is Lid, not Jid */
382
+ newsletterDemote: async (jid, user) => {
383
+ await newsletterWMexQuery(jid, QueryIds.DEMOTE, {
384
+ user_id: user
385
+ });
386
+ },
387
+
388
+ newsletterDelete: async (jid) => {
389
+ await newsletterWMexQuery(jid, QueryIds.DELETE);
390
+ },
391
+
392
+ /** if code wasn't passed, the reaction will be removed (if is reacted) */
393
+ newsletterReactMessage: async (jid, server_id, code) => {
394
+ await query({
395
+ tag: 'message',
396
+ attrs: {
397
+ to: jid,
398
+ ...(!code ? { edit: '7' } : {}),
399
+ type: 'reaction',
400
+ server_id,
401
+ id: generateMessageID()
402
+ },
403
+ content: [{
404
+ tag: 'reaction',
405
+ attrs: code ? { code } : {}
406
+ }]
407
+ });
408
+ },
409
+
410
+ newsletterFetchMessages: async (type, key, count, after) => {
411
+ const afterStr = after?.toString();
412
+
413
+ const result = await newsletterQuery(
414
+ S_WHATSAPP_NET,
415
+ 'get',
416
+ [
417
+ {
418
+ tag: 'messages',
419
+ attrs: {
420
+ type,
421
+ ...(type === 'invite'
422
+ ? { key }
423
+ : { jid: key }),
424
+ count: count.toString(),
425
+ after: afterStr || '100'
426
+ }
427
+ }
428
+ ]
429
+ );
430
+
431
+ return await parseFetchedUpdates(result, 'messages');
432
+ },
433
+
434
+ newsletterFetchUpdates: async (jid, count, after, since) => {
435
+ const result = await newsletterQuery(
436
+ jid,
437
+ 'get',
438
+ [
439
+ {
440
+ tag: 'message_updates',
441
+ attrs: {
442
+ count: count.toString(),
443
+ after: after?.toString() || '100',
444
+ since: since?.toString() || '0'
445
+ }
446
+ }
447
+ ]
448
+ );
449
+
450
+ return await parseFetchedUpdates(result, 'updates');
451
+ }
452
+ };
453
+ };
454
+
455
+ export const extractNewsletterMetadata = (node, isCreate) => {
456
+ const result = getBinaryNodeChild(node, 'result')
457
+ ?.content
458
+ ?.toString();
459
+
460
+ // FIX: guard kalau result kosong, biar error-nya jelas.
461
+ if (!result) {
462
+ throw new Error('extractNewsletterMetadata: missing result node');
463
+ }
464
+
465
+ const parsed = JSON.parse(result);
466
+ const metadataPath = parsed?.data?.[isCreate ? XWAPaths.CREATE : XWAPaths.NEWSLETTER];
467
+
468
+ if (!metadataPath) {
469
+ throw new Error('extractNewsletterMetadata: missing metadata path');
470
+ }
471
+
472
+ const thread = metadataPath.thread_metadata ?? {};
473
+
474
+ // FIX: semua field pakai optional chaining biar nggak crash kalau kosong.
475
+ const metadata = {
476
+ id: metadataPath.id,
477
+ state: metadataPath.state?.type ?? null,
478
+ creation_time: +(thread.creation_time ?? 0),
479
+ name: thread.name?.text ?? '',
480
+ nameTime: +(thread.name?.update_time ?? 0),
481
+ description: thread.description?.text ?? '',
482
+ descriptionTime: +(thread.description?.update_time ?? 0),
483
+ invite: thread.invite ?? null,
484
+ handle: thread.handle ?? null,
485
+ picture: thread.picture?.direct_path || null,
486
+ preview: thread.preview?.direct_path || null,
487
+ reaction_codes: thread.settings?.reaction_codes?.value ?? null,
488
+ subscribers: +(thread.subscribers_count ?? 0),
489
+ verification: thread.verification ?? null,
490
+ viewer_metadata: metadataPath.viewer_metadata ?? null
491
+ };
492
+
493
+ return metadata;
494
+ };