@itsliaaa/baileys 0.1.33 → 0.2.1

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.
@@ -1,24 +1,33 @@
1
1
  import NodeCache from '@cacheable/node-cache';
2
2
  import { Boom } from '@hapi/boom';
3
3
  import { proto } from '../../WAProto/index.js';
4
- import { DEFAULT_CACHE_TTLS, PROCESSABLE_HISTORY_TYPES } from '../Defaults/index.js';
4
+ import { DEFAULT_CACHE_TTLS, HISTORY_SYNC_PAUSED_TIMEOUT_MS, PROCESSABLE_HISTORY_TYPES } from '../Defaults/index.js';
5
5
  import { ALL_WA_PATCH_NAMES } from '../Types/index.js';
6
6
  import { SyncState } from '../Types/State.js';
7
- import { chatModificationToAppPatch, decodePatches, decodeSyncdSnapshot, encodeSyncdPatch, extractSyncdPatches, generateProfilePicture, getHistoryMsg, newLTHashState, processSyncAction } from '../Utils/index.js';
7
+ import { chatModificationToAppPatch, decodePatches, decodeSyncdSnapshot, encodeSyncdPatch, ensureLTHashStateVersion, extractSyncdPatches, generateProfilePicture, getHistoryMsg, isAppStateSyncIrrecoverable, isMissingKeyError, MAX_SYNC_ATTEMPTS, newLTHashState, processSyncAction } from '../Utils/index.js';
8
8
  import { makeMutex } from '../Utils/make-mutex.js';
9
9
  import processMessage from '../Utils/process-message.js';
10
10
  import { buildTcTokenFromJid } from '../Utils/tc-token-utils.js';
11
11
  import { getBinaryNodeChild, getBinaryNodeChildren, isPnUser, isLidUser, isHostedLidUser, isHostedPnUser, jidDecode, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary/index.js';
12
12
  import { USyncQuery, USyncUser } from '../WAUSync/index.js';
13
13
  import { makeSocket } from './socket.js';
14
- const MAX_SYNC_ATTEMPTS = 2;
15
14
  // Lia@Note 08-02-26 --- I know it's not efficient for RSS ಥ⁠‿⁠ಥ
16
15
  const USER_ID_CACHE = new Map();
17
16
  export const makeChatsSocket = (config) => {
18
17
  const { logger, markOnlineOnConnect, fireInitQueries, appStateMacVerification, shouldIgnoreJid, shouldSyncHistoryMessage, getMessage } = config;
19
18
  const sock = makeSocket(config);
20
19
  const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError, sendUnifiedSession, registerSocketEndHandler } = sock;
20
+ const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
21
21
  let privacySettings;
22
+ /** Server-assigned AB props for protocol behavior. */
23
+ const serverProps = {
24
+ /** AB prop 10518: gate tctoken on 1:1 messages. Default true (safe: avoids 463). */
25
+ privacyTokenOn1to1: true,
26
+ /** AB prop 9666: gate tctoken on profile picture IQs. WA Web default: true. */
27
+ profilePicPrivacyToken: true,
28
+ /** AB prop 14303: issue tctokens to LID instead of PN. WA Web default: false. */
29
+ lidTrustedTokenIssueToLid: false
30
+ };
22
31
  let syncState = SyncState.Connecting;
23
32
  /** this mutex ensures that messages are processed in order */
24
33
  const messageMutex = makeMutex();
@@ -30,6 +39,15 @@ export const makeChatsSocket = (config) => {
30
39
  const notificationMutex = makeMutex();
31
40
  // Timeout for AwaitingInitialSync state
32
41
  let awaitingSyncTimeout;
42
+ // In-memory history sync completion tracking (resets on reconnection)
43
+ const historySyncStatus = {
44
+ initialBootstrapComplete: false,
45
+ recentSyncComplete: false
46
+ };
47
+ let historySyncPausedTimeout;
48
+ // Collections blocked on missing app state sync keys (mirrors WA Web's "Blocked" state).
49
+ // When a key arrives via APP_STATE_SYNC_KEY_SHARE, these are re-synced.
50
+ const blockedCollections = new Set();
33
51
  const placeholderResendCache = config.placeholderResendCache ||
34
52
  new NodeCache({
35
53
  stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
@@ -438,6 +456,9 @@ export const makeChatsSocket = (config) => {
438
456
  const collectionsToHandle = new Set(collections);
439
457
  // in case something goes wrong -- ensure we don't enter a loop that cannot be exited from
440
458
  const attemptsMap = {};
459
+ // collections that failed and need a full snapshot on retry
460
+ // mirrors WA Web's ErrorFatal -> force snapshot behavior
461
+ const forceSnapshotCollections = new Set();
441
462
  // keep executing till all collections are done
442
463
  // sometimes a single patch request will not return all the patches (God knows why)
443
464
  // so we fetch till they're all done (this is determined by the "has_more_patches" flag)
@@ -448,6 +469,7 @@ export const makeChatsSocket = (config) => {
448
469
  const result = await authState.keys.get('app-state-sync-version', [name]);
449
470
  let state = result[name];
450
471
  if (state) {
472
+ state = ensureLTHashStateVersion(state);
451
473
  if (typeof initialVersionMap[name] === 'undefined') {
452
474
  initialVersionMap[name] = state.version;
453
475
  }
@@ -456,14 +478,18 @@ export const makeChatsSocket = (config) => {
456
478
  state = newLTHashState();
457
479
  }
458
480
  states[name] = state;
459
- logger.info(`resyncing ${name} from v${state.version}`);
481
+ const shouldForceSnapshot = forceSnapshotCollections.has(name);
482
+ if (shouldForceSnapshot) {
483
+ forceSnapshotCollections.delete(name);
484
+ }
485
+ logger.info(`resyncing ${name} from v${state.version}${shouldForceSnapshot ? ' (forcing snapshot)' : ''}`);
460
486
  nodes.push({
461
487
  tag: 'collection',
462
488
  attrs: {
463
489
  name,
464
490
  version: state.version.toString(),
465
- // return snapshot if being synced from scratch
466
- return_snapshot: (!state.version).toString()
491
+ // return snapshot if syncing from scratch or forcing after a failed attempt
492
+ return_snapshot: (shouldForceSnapshot || !state.version).toString()
467
493
  }
468
494
  });
469
495
  }
@@ -512,19 +538,37 @@ export const makeChatsSocket = (config) => {
512
538
  }
513
539
  }
514
540
  catch (error) {
515
- // if retry attempts overshoot
516
- // or key not found
517
- const isIrrecoverableError = attemptsMap[name] >= MAX_SYNC_ATTEMPTS ||
518
- error.output?.statusCode === 404 ||
519
- error.name === 'TypeError';
520
- logger.info({ name, error: error.stack }, `failed to sync state from version${isIrrecoverableError ? '' : ', removing and trying from scratch'}`);
521
- await authState.keys.set({ 'app-state-sync-version': { [name]: null } });
522
- // increment number of retries
523
541
  attemptsMap[name] = (attemptsMap[name] || 0) + 1;
524
- if (isIrrecoverableError) {
525
- // stop retrying
542
+ const logData = {
543
+ name,
544
+ attempt: attemptsMap[name],
545
+ version: states[name].version,
546
+ statusCode: error.output?.statusCode,
547
+ errorType: error.name,
548
+ error: error.stack
549
+ };
550
+ if (isMissingKeyError(error) && attemptsMap[name] >= MAX_SYNC_ATTEMPTS) {
551
+ // WA Web treats missing keys as "Blocked" — park the collection
552
+ // until the key arrives via APP_STATE_SYNC_KEY_SHARE.
553
+ logger.warn(logData, `${name} blocked on missing key from v${states[name].version}, parking after ${attemptsMap[name]} attempts`);
554
+ blockedCollections.add(name);
555
+ collectionsToHandle.delete(name);
556
+ }
557
+ else if (isMissingKeyError(error)) {
558
+ // Retry with a snapshot which may use a different key.
559
+ logger.info(logData, `${name} blocked on missing key from v${states[name].version}, retrying with snapshot`);
560
+ forceSnapshotCollections.add(name);
561
+ }
562
+ else if (isAppStateSyncIrrecoverable(error, attemptsMap[name])) {
563
+ logger.warn(logData, `failed to sync ${name} from v${states[name].version}, giving up`);
526
564
  collectionsToHandle.delete(name);
527
565
  }
566
+ else {
567
+ logger.info(logData, `failed to sync ${name} from v${states[name].version}, forcing snapshot retry`);
568
+ // force a full snapshot on retry to recover from
569
+ // corrupted local state (e.g. LTHash MAC mismatch)
570
+ forceSnapshotCollections.add(name);
571
+ }
528
572
  }
529
573
  }
530
574
  }
@@ -540,42 +584,34 @@ export const makeChatsSocket = (config) => {
540
584
  * type = "image for the high res picture"
541
585
  */
542
586
  const profilePictureUrl = async (jid, type = 'image', timeoutMs) => {
543
- // Lia@Changes 06-02-26 --- Refactor profilePictureUrl() to use tctoken and adjust error handling
544
- jid = jidNormalizedUser(jid);
545
- const baseContent = {
546
- tag: 'picture',
547
- attrs: {
548
- type,
549
- query: 'url'
550
- }
551
- };
552
- const tcTokenData = await authState.keys.get('tctoken', [jid]);
553
- const tcTokenBuffer = tcTokenData?.[jid]?.token
554
- if (tcTokenBuffer) {
555
- baseContent.content = [{
556
- tag: 'tctoken',
557
- attrs: {},
558
- content: tcTokenBuffer
559
- }];
587
+ const baseContent = [{ tag: 'picture', attrs: { type, query: 'url' } }];
588
+ // WA Web only includes tctoken for user JIDs (not groups/newsletters)
589
+ // and never for own profile pic (Chat model for self has no tcToken).
590
+ // Including tctoken for own JID causes the server to never respond.
591
+ const normalizedJid = jidNormalizedUser(jid);
592
+ const isUserJid = isPnUser(normalizedJid) || isLidUser(normalizedJid);
593
+ const me = authState.creds.me;
594
+ const isSelf = me && (normalizedJid === jidNormalizedUser(me.id) || (me.lid && normalizedJid === jidNormalizedUser(me.lid)));
595
+ let content = baseContent;
596
+ if (serverProps.profilePicPrivacyToken && isUserJid && !isSelf) {
597
+ content = await buildTcTokenFromJid({
598
+ authState,
599
+ jid: normalizedJid,
600
+ baseContent,
601
+ getLIDForPN
602
+ });
560
603
  }
561
604
  const result = await query({
562
605
  tag: 'iq',
563
606
  attrs: {
564
- target: jid,
607
+ target: normalizedJid,
565
608
  to: S_WHATSAPP_NET,
566
609
  type: 'get',
567
610
  xmlns: 'w:profile:picture'
568
611
  },
569
- content: [baseContent]
612
+ content
570
613
  }, timeoutMs);
571
614
  const child = getBinaryNodeChild(result, 'picture');
572
- if (!child) {
573
- throw new Boom('Picture node missing', { statusCode: 404 });
574
- }
575
- const status = child.attrs?.status;
576
- if (status === '404' || status === '204') {
577
- throw new Boom('Profile picture not set', { statusCode: 404 });
578
- }
579
615
  return child?.attrs?.url;
580
616
  };
581
617
  const createCallLink = async (type, event, timeoutMs) => {
@@ -639,7 +675,12 @@ export const makeChatsSocket = (config) => {
639
675
  * @param tcToken token for subscription, use if present
640
676
  */
641
677
  const presenceSubscribe = async (toJid) => {
642
- const tcTokenContent = await buildTcTokenFromJid({ authState, jid: toJid });
678
+ // Only include tctoken for user JIDs groups/newsletters don't use tctokens
679
+ const normalizedToJid = jidNormalizedUser(toJid);
680
+ const isUserJid = isPnUser(normalizedToJid) || isLidUser(normalizedToJid);
681
+ const tcTokenContent = isUserJid
682
+ ? await buildTcTokenFromJid({ authState, jid: normalizedToJid, getLIDForPN })
683
+ : undefined;
643
684
  return sendNode({
644
685
  tag: 'presence',
645
686
  attrs: {
@@ -694,7 +735,7 @@ export const makeChatsSocket = (config) => {
694
735
  logger.debug({ patch: patchCreate }, 'applying app patch');
695
736
  await resyncAppState([name], false);
696
737
  const { [name]: currentSyncVersion } = await authState.keys.get('app-state-sync-version', [name]);
697
- initial = currentSyncVersion || newLTHashState();
738
+ initial = currentSyncVersion ? ensureLTHashStateVersion(currentSyncVersion) : newLTHashState();
698
739
  encodeResult = await encodeSyncdPatch(patchCreate, myAppStateKeyId, initial, getAppStateSyncKey);
699
740
  const { patch, state } = encodeResult;
700
741
  const node = {
@@ -740,22 +781,21 @@ export const makeChatsSocket = (config) => {
740
781
  }
741
782
  }
742
783
  };
743
- /** sending non-abt props may fix QR scan fail if server expects */
784
+ /** fetch AB props */
744
785
  const fetchProps = async () => {
745
- //TODO: implement both protocol 1 and protocol 2 prop fetching, specially for abKey for WM
746
786
  const resultNode = await query({
747
787
  tag: 'iq',
748
788
  attrs: {
749
789
  to: S_WHATSAPP_NET,
750
- xmlns: 'w',
790
+ xmlns: 'abt',
751
791
  type: 'get'
752
792
  },
753
793
  content: [
754
794
  {
755
795
  tag: 'props',
756
796
  attrs: {
757
- protocol: '2',
758
- hash: authState?.creds?.lastPropHash || ''
797
+ protocol: '1',
798
+ ...(authState?.creds?.lastPropHash ? { hash: authState.creds.lastPropHash } : {})
759
799
  }
760
800
  }
761
801
  ]
@@ -770,7 +810,20 @@ export const makeChatsSocket = (config) => {
770
810
  }
771
811
  props = reduceBinaryNodeToDictionary(propsNode, 'prop');
772
812
  }
773
- logger.debug('fetched props');
813
+ // Extract protocol-relevant AB props (only the ones we need)
814
+ const privacyTokenProp = props['10518'] ?? props['privacy_token_sending_on_all_1_on_1_messages'];
815
+ if (privacyTokenProp !== undefined) {
816
+ serverProps.privacyTokenOn1to1 = privacyTokenProp === 'true' || privacyTokenProp === '1';
817
+ }
818
+ const profilePicProp = props['9666'] ?? props['profile_scraping_privacy_token_in_photo_iq'];
819
+ if (profilePicProp !== undefined) {
820
+ serverProps.profilePicPrivacyToken = profilePicProp === 'true' || profilePicProp === '1';
821
+ }
822
+ const lidIssueProp = props['14303'] ?? props['lid_trusted_token_issue_to_lid'];
823
+ if (lidIssueProp !== undefined) {
824
+ serverProps.lidTrustedTokenIssueToLid = lidIssueProp === 'true' || lidIssueProp === '1';
825
+ }
826
+ logger.debug({ serverProps }, 'fetched props');
774
827
  return props;
775
828
  };
776
829
  /**
@@ -910,6 +963,44 @@ export const makeChatsSocket = (config) => {
910
963
  ? shouldSyncHistoryMessage(historyMsg) &&
911
964
  PROCESSABLE_HISTORY_TYPES.includes(historyMsg.syncType)
912
965
  : false;
966
+ if (historyMsg && shouldProcessHistoryMsg) {
967
+ const syncType = historyMsg.syncType;
968
+ // INITIAL_BOOTSTRAP — fire immediately, no progress check (same as WA Web K function)
969
+ if (syncType === proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP && !historySyncStatus.initialBootstrapComplete) {
970
+ historySyncStatus.initialBootstrapComplete = true;
971
+ ev.emit('messaging-history.status', {
972
+ syncType,
973
+ status: 'complete',
974
+ explicit: true
975
+ });
976
+ }
977
+ // RECENT with progress === 100 — explicit completion
978
+ if (syncType === proto.HistorySync.HistorySyncType.RECENT && historyMsg.progress === 100 && !historySyncStatus.recentSyncComplete) {
979
+ historySyncStatus.recentSyncComplete = true;
980
+ clearTimeout(historySyncPausedTimeout);
981
+ historySyncPausedTimeout = undefined;
982
+ ev.emit('messaging-history.status', {
983
+ syncType,
984
+ status: 'complete',
985
+ explicit: true
986
+ });
987
+ }
988
+ // Reset 120s paused timeout on any RECENT chunk (like WA Web's handleChunkProgress)
989
+ if (syncType === proto.HistorySync.HistorySyncType.RECENT && !historySyncStatus.recentSyncComplete) {
990
+ clearTimeout(historySyncPausedTimeout);
991
+ historySyncPausedTimeout = setTimeout(() => {
992
+ if (!historySyncStatus.recentSyncComplete) {
993
+ historySyncStatus.recentSyncComplete = true;
994
+ ev.emit('messaging-history.status', {
995
+ syncType: proto.HistorySync.HistorySyncType.RECENT,
996
+ status: 'paused',
997
+ explicit: false
998
+ });
999
+ }
1000
+ historySyncPausedTimeout = undefined;
1001
+ }, HISTORY_SYNC_PAUSED_TIMEOUT_MS);
1002
+ }
1003
+ }
913
1004
  // State machine: decide on sync and flush
914
1005
  if (historyMsg && syncState === SyncState.AwaitingInitialSync) {
915
1006
  if (awaitingSyncTimeout) {
@@ -929,6 +1020,8 @@ export const makeChatsSocket = (config) => {
929
1020
  }
930
1021
  const doAppStateSync = async () => {
931
1022
  if (syncState === SyncState.Syncing) {
1023
+ // All collections will be synced, so clear any blocked ones
1024
+ blockedCollections.clear();
932
1025
  logger.info('Doing app state sync');
933
1026
  await resyncAppState(ALL_WA_PATCH_NAMES, true);
934
1027
  // Sync is complete, go online and flush everything
@@ -988,6 +1081,11 @@ export const makeChatsSocket = (config) => {
988
1081
  }
989
1082
  });
990
1083
  ev.on('connection.update', ({ connection, receivedPendingNotifications }) => {
1084
+ if (connection === 'close') {
1085
+ blockedCollections.clear();
1086
+ clearTimeout(historySyncPausedTimeout);
1087
+ historySyncPausedTimeout = undefined;
1088
+ }
991
1089
  if (connection === 'open') {
992
1090
  if (fireInitQueries) {
993
1091
  executeInitQueries().catch(error => onUnexpectedError(error, 'init queries'));
@@ -997,6 +1095,10 @@ export const makeChatsSocket = (config) => {
997
1095
  if (!receivedPendingNotifications || syncState !== SyncState.Connecting) {
998
1096
  return;
999
1097
  }
1098
+ historySyncStatus.initialBootstrapComplete = false;
1099
+ historySyncStatus.recentSyncComplete = false;
1100
+ clearTimeout(historySyncPausedTimeout);
1101
+ historySyncPausedTimeout = undefined;
1000
1102
  syncState = SyncState.AwaitingInitialSync;
1001
1103
  logger.info('Connection is now AwaitingInitialSync, buffering events');
1002
1104
  ev.buffer();
@@ -1009,19 +1111,49 @@ export const makeChatsSocket = (config) => {
1009
1111
  setTimeout(() => ev.flush(), 0);
1010
1112
  return;
1011
1113
  }
1012
- logger.info('History sync is enabled, awaiting notification with a 20s timeout.');
1114
+ // On reconnection (accountSyncCounter > 0), the server does not push
1115
+ // history sync notifications — the device already has its data.
1116
+ // Skip the 20s wait and go online immediately.
1117
+ if (authState.creds.accountSyncCounter > 0) {
1118
+ logger.info('Reconnection with existing sync data, skipping history sync wait. Transitioning to Online.');
1119
+ syncState = SyncState.Online;
1120
+ setTimeout(() => ev.flush(), 0);
1121
+ return;
1122
+ }
1123
+ logger.info('First connection, awaiting history sync notification with a 20s timeout.');
1013
1124
  if (awaitingSyncTimeout) {
1014
1125
  clearTimeout(awaitingSyncTimeout);
1015
1126
  }
1016
1127
  awaitingSyncTimeout = setTimeout(() => {
1017
1128
  if (syncState === SyncState.AwaitingInitialSync) {
1018
- // TODO: investigate
1019
1129
  logger.warn('Timeout in AwaitingInitialSync, forcing state to Online and flushing buffer');
1020
1130
  syncState = SyncState.Online;
1021
1131
  ev.flush();
1132
+ // Increment so subsequent reconnections skip the 20s wait.
1133
+ // Late-arriving history is still processed via processMessage
1134
+ // regardless of the state machine phase.
1135
+ const accountSyncCounter = (authState.creds.accountSyncCounter || 0) + 1;
1136
+ ev.emit('creds.update', { accountSyncCounter });
1022
1137
  }
1023
1138
  }, 20000);
1024
1139
  });
1140
+ // When an app state sync key arrives (myAppStateKeyId is set) and there are
1141
+ // collections blocked on a missing key, trigger a re-sync for just those collections.
1142
+ // This mirrors WA Web's Blocked → retry-on-key-arrival behavior.
1143
+ ev.on('creds.update', ({ myAppStateKeyId }) => {
1144
+ if (!myAppStateKeyId || blockedCollections.size === 0) {
1145
+ return;
1146
+ }
1147
+ // If we're in the middle of a full sync, doAppStateSync handles all collections
1148
+ if (syncState === SyncState.Syncing) {
1149
+ blockedCollections.clear();
1150
+ return;
1151
+ }
1152
+ const collections = [...blockedCollections];
1153
+ blockedCollections.clear();
1154
+ logger.info({ collections }, 'app state sync key arrived, re-syncing blocked collections');
1155
+ resyncAppState(collections, false).catch(error => onUnexpectedError(error, 'blocked collections resync'));
1156
+ });
1025
1157
  ev.on('lid-mapping.update', async ({ lid, pn }) => {
1026
1158
  try {
1027
1159
  await signalRepository.lidMapping.storeLIDPNMappings([{ lid, pn }]);
@@ -1043,6 +1175,7 @@ export const makeChatsSocket = (config) => {
1043
1175
  });
1044
1176
  return {
1045
1177
  ...sock,
1178
+ serverProps,
1046
1179
  createCallLink,
1047
1180
  getBotListV2,
1048
1181
  messageMutex,
@@ -277,11 +277,13 @@ export const extractGroupMetadata = (result) => {
277
277
  let descId;
278
278
  let descOwner;
279
279
  let descOwnerPn;
280
+ let descOwnerUsername;
280
281
  let descTime;
281
282
  if (descChild) {
282
283
  desc = getBinaryNodeChildString(descChild, 'body');
283
284
  descOwner = descChild.attrs.participant ? jidNormalizedUser(descChild.attrs.participant) : undefined;
284
285
  descOwnerPn = descChild.attrs.participant_pn ? jidNormalizedUser(descChild.attrs.participant_pn) : undefined;
286
+ descOwnerUsername = descChild.attrs.participant_username || undefined;
285
287
  descTime = +descChild.attrs.t;
286
288
  descId = descChild.attrs.id;
287
289
  }
@@ -295,16 +297,19 @@ export const extractGroupMetadata = (result) => {
295
297
  subject: group.attrs.subject,
296
298
  subjectOwner: group.attrs.s_o,
297
299
  subjectOwnerPn: group.attrs.s_o_pn,
300
+ subjectOwnerUsername: group.attrs.s_o_username,
298
301
  subjectTime: +group.attrs.s_t,
299
302
  size: group.attrs.size ? +group.attrs.size : getBinaryNodeChildren(group, 'participant').length,
300
303
  creation: +group.attrs.creation,
301
304
  owner: group.attrs.creator ? jidNormalizedUser(group.attrs.creator) : undefined,
302
305
  ownerPn: group.attrs.creator_pn ? jidNormalizedUser(group.attrs.creator_pn) : undefined,
306
+ ownerUsername: group.attrs.creator_username || undefined,
303
307
  owner_country_code: group.attrs.creator_country_code,
304
308
  desc,
305
309
  descId,
306
310
  descOwner,
307
311
  descOwnerPn,
312
+ descOwnerUsername,
308
313
  descTime,
309
314
  linkedParent: getBinaryNodeChild(group, 'linked_parent')?.attrs.jid || undefined,
310
315
  restrict: !!getBinaryNodeChild(group, 'locked'),
@@ -319,6 +324,7 @@ export const extractGroupMetadata = (result) => {
319
324
  id: attrs.jid,
320
325
  phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined,
321
326
  lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined,
327
+ username: attrs.participant_username || attrs.username || undefined,
322
328
  admin: (attrs.type || null)
323
329
  };
324
330
  }),