11import { Request , Response } from 'express'
22import { UNOAPI_META_GROUPS_ENABLED } from '../defaults'
3- import { getContactInfo , getContactName , getGroup , getLidForPn , getPnForLid , getProfilePicture , redisKeys , BASE_KEY , setGroup } from '../services/redis'
3+ import { getContactInfo , getContactName , getGroup , getLidForPn , getPnForLid , getProfilePicture , redisKeys , BASE_KEY , setGroup , redisSetIfNotExists , redisDelKey , groupKey } from '../services/redis'
44import { normalizeGroupId , normalizeParticipantId } from '../services/transformer'
55import { Incoming } from '../services/incoming'
66import { Outgoing } from '../services/outgoing'
@@ -88,6 +88,69 @@ const participantRole = (participant: any): string => {
8888 return 'member'
8989}
9090
91+ const resolveParticipantIdentity = async ( phone : string , participant : any ) => {
92+ const rawId = `${ participant ?. id || participant ?. jid || participant ?. lid || '' } ` . trim ( )
93+ const rawPhoneNumber = `${ participant ?. phoneNumber || participant ?. phone_number || participant ?. pn || '' } ` . trim ( )
94+ const sourceJid = rawId || rawPhoneNumber
95+ let pnJid = rawPhoneNumber . endsWith ( '@s.whatsapp.net' ) ? rawPhoneNumber : ''
96+ if ( ! pnJid && rawPhoneNumber ) {
97+ const candidate = normalizeParticipantJidForBaileys ( rawPhoneNumber )
98+ if ( candidate . endsWith ( '@s.whatsapp.net' ) ) pnJid = candidate
99+ }
100+ if ( ! pnJid && sourceJid . endsWith ( '@s.whatsapp.net' ) ) pnJid = sourceJid
101+ let lid = participantLid ( participant , rawId || sourceJid )
102+
103+ if ( ! pnJid && sourceJid && ! sourceJid . endsWith ( '@lid' ) ) {
104+ const candidate = normalizeParticipantJidForBaileys ( sourceJid )
105+ if ( candidate . endsWith ( '@s.whatsapp.net' ) ) pnJid = candidate
106+ }
107+
108+ if ( ! pnJid && lid ) {
109+ try { pnJid = `${ await getPnForLid ( phone , lid ) || '' } ` . trim ( ) } catch { }
110+ }
111+
112+ if ( ! lid && pnJid ) {
113+ try { lid = `${ await getLidForPn ( phone , pnJid ) || '' } ` . trim ( ) } catch { }
114+ }
115+
116+ const waId = normalizeParticipantPhoneForResponse ( pnJid || sourceJid )
117+ return {
118+ sourceJid,
119+ pnJid,
120+ lid,
121+ waId,
122+ responseJid : normalizeParticipantJidForResponse ( pnJid || sourceJid || lid ) ,
123+ }
124+ }
125+
126+ const participantDisplayName = ( participant : any ) : string => {
127+ return firstNonEmptyString (
128+ participant ?. name ,
129+ participant ?. notify ,
130+ participant ?. verifiedName ,
131+ participant ?. pushName ,
132+ ) || ''
133+ }
134+
135+ const resolveParticipantName = async ( phone : string , participant : any , pnJid : string , lid : string ) : Promise < string > => {
136+ const directName = participantDisplayName ( participant )
137+ if ( directName ) return directName
138+ for ( const jid of [ pnJid , lid ] ) {
139+ const clean = `${ jid || '' } ` . trim ( )
140+ if ( ! clean ) continue
141+ let name = ''
142+ try { name = `${ await getContactName ( phone , clean ) || '' } ` . trim ( ) } catch { }
143+ if ( ! name ) {
144+ try {
145+ const infoRaw = await getContactInfo ( phone , clean )
146+ name = `${ parseContactInfoName ( infoRaw ) || '' } ` . trim ( )
147+ } catch { }
148+ }
149+ if ( name ) return name
150+ }
151+ return ''
152+ }
153+
91154const groupDescription = ( group : any ) : string => {
92155 return `${ group ?. desc || group ?. description || '' } `
93156}
@@ -121,6 +184,9 @@ const inviteLinkFromCode = (code?: string): string => {
121184}
122185
123186const nowTimestamp = ( ) => `${ Math . floor ( Date . now ( ) / 1000 ) } `
187+ const GROUP_METADATA_REFRESH_THROTTLE_SECONDS = 60
188+ const GROUP_METADATA_REFRESH_TIMEOUT_MS = 5000
189+ const GROUP_PARTICIPANTS_NAME_LOOKUP_LIMIT = 100
124190
125191const managementWebhook = ( phone : string , field : string , value : any ) => ( {
126192 object : 'whatsapp_business_account' ,
@@ -196,23 +262,38 @@ export class GroupsController {
196262 return `${ group ?. profilePicture || group ?. picture || cached || '' } `
197263 }
198264
199- private async formatParticipant ( phone : string , participant : any , options : { includePicture ?: boolean } = { } ) {
200- const sourceJid = `${ participant ?. id || participant ?. jid || participant ?. lid || '' } ` . trim ( )
201- const jid = normalizeParticipantJidForResponse ( sourceJid )
202- const pnJid = sourceJid . endsWith ( '@s.whatsapp.net' ) ? sourceJid : ''
203- const lid = participantLid ( participant , sourceJid )
204- const waId = normalizeParticipantPhoneForResponse ( pnJid || sourceJid )
265+ private async refreshGroupMetadata ( phone : string , groupJid : string ) : Promise < any | undefined > {
266+ if ( typeof this . incoming . groupMetadata !== 'function' ) return undefined
267+ const refreshKey = `${ BASE_KEY } group-refresh:${ phone } :${ groupJid } `
268+ const acquired = await redisSetIfNotExists ( refreshKey , `${ Date . now ( ) } ` , GROUP_METADATA_REFRESH_THROTTLE_SECONDS )
269+ if ( ! acquired ) return undefined
270+ const timeout = new Promise < undefined > ( ( resolve ) => setTimeout ( ( ) => resolve ( undefined ) , GROUP_METADATA_REFRESH_TIMEOUT_MS ) )
271+ const fetched = await Promise . race ( [
272+ this . incoming . groupMetadata ( phone , groupJid ) . catch ( ( ) => undefined ) ,
273+ timeout ,
274+ ] )
275+ if ( ! fetched ) return undefined
276+ try { await redisDelKey ( groupKey ( phone , groupJid ) ) } catch { }
277+ await setGroup ( phone , groupJid , fetched as any )
278+ return fetched
279+ }
280+
281+ private async formatParticipant ( phone : string , participant : any , options : { includePicture ?: boolean , resolveName ?: boolean } = { } ) {
282+ const { sourceJid, pnJid, lid, waId, responseJid } = await resolveParticipantIdentity ( phone , participant )
283+ const shouldResolveName = options . resolveName !== false
205284 let contactInfo : any
206- try { contactInfo = parseContactInfo ( await getContactInfo ( phone , sourceJid ) ) } catch { }
285+ if ( shouldResolveName ) {
286+ try { contactInfo = parseContactInfo ( await getContactInfo ( phone , pnJid || sourceJid || lid ) ) } catch { }
287+ }
207288 const username = participantUsername ( participant , contactInfo )
208- const resolvedName = await resolveNameForJid ( phone , sourceJid )
289+ const resolvedName = shouldResolveName ? await resolveParticipantName ( phone , participant , pnJid , lid ) : participantDisplayName ( participant )
209290 const name = firstNonEmptyString ( resolvedName , username , waId , lid ) || ''
210- const picture = options . includePicture ? await getProfilePicture ( phone , sourceJid ) : ''
291+ const picture = options . includePicture ? await getProfilePicture ( phone , pnJid || sourceJid || lid ) : ''
211292 return {
212- jid,
293+ jid : responseJid ,
213294 wa_id : waId ,
295+ user_id : lid ,
214296 name,
215- ...( lid ? { user_id : lid } : { } ) ,
216297 ...( username ? { username } : { } ) ,
217298 ...( picture ? { picture } : { } ) ,
218299 ...( lid ? { lid } : { } ) ,
@@ -222,14 +303,9 @@ export class GroupsController {
222303 }
223304
224305 private async formatParticipantReference ( phone : string , rawJid : string ) {
225- const clean = `${ rawJid || '' } ` . trim ( )
226- const waId = normalizeParticipantPhoneForResponse ( clean )
227- let userId = clean . endsWith ( '@lid' ) ? clean : ''
228- if ( ! userId && clean . endsWith ( '@s.whatsapp.net' ) ) {
229- try { userId = `${ await getLidForPn ( phone , clean ) || '' } ` . trim ( ) } catch { }
230- }
306+ const { waId, lid } = await resolveParticipantIdentity ( phone , { id : rawJid } )
231307 const response : any = { wa_id : waId }
232- if ( userId ) response . user_id = userId
308+ if ( lid ) response . user_id = lid
233309 return response
234310 }
235311
@@ -427,18 +503,25 @@ export class GroupsController {
427503 if ( ! phone ) return res . status ( 400 ) . json ( { error : 'missing phone param' } )
428504 if ( ! groupId ) return res . status ( 400 ) . json ( { error : 'missing groupId param' } )
429505 const groupJid = normalizeGroupJid ( groupId )
430- const group = await getGroup ( phone , groupJid )
506+ const cachedGroup = await getGroup ( phone , groupJid )
507+ const cachedParticipants = Array . isArray ( ( cachedGroup as any ) ?. participants ) ? ( cachedGroup as any ) . participants : [ ]
508+ const shouldRefreshMetadata =
509+ ! cachedGroup ||
510+ cachedParticipants . length <= GROUP_PARTICIPANTS_NAME_LOOKUP_LIMIT ||
511+ queryBoolean ( req . query . refresh_metadata || req . query . refreshMetadata )
512+ const group = shouldRefreshMetadata ? await this . refreshGroupMetadata ( phone , groupJid ) || cachedGroup : cachedGroup
431513 if ( ! group ) return res . status ( 404 ) . json (
432514 UNOAPI_META_GROUPS_ENABLED
433515 ? { error : 'group not found in cache' , group_id : groupJid }
434516 : { error : 'group not found in cache' , groupJid }
435517 )
436518
437519 const participantsRaw : any [ ] = Array . isArray ( ( group as any ) ?. participants ) ? ( group as any ) . participants : [ ]
520+ const resolveParticipantNames = participantsRaw . length <= GROUP_PARTICIPANTS_NAME_LOOKUP_LIMIT || queryBoolean ( req . query . resolve_names || req . query . resolveNames )
438521 if ( UNOAPI_META_GROUPS_ENABLED ) {
439522 const picture = await this . groupPicture ( phone , groupJid , group )
440523 const includeParticipantPictures = queryBoolean ( req . query . include_pictures )
441- const participants = await Promise . all ( participantsRaw . map ( ( participant : any ) => this . formatParticipant ( phone , participant , { includePicture : includeParticipantPictures } ) ) )
524+ const participants = await Promise . all ( participantsRaw . map ( ( participant : any ) => this . formatParticipant ( phone , participant , { includePicture : includeParticipantPictures , resolveName : resolveParticipantNames } ) ) )
442525 return res . json ( {
443526 phone,
444527 group : {
@@ -452,10 +535,13 @@ export class GroupsController {
452535 } )
453536 }
454537 const participants = await Promise . all ( participantsRaw . map ( async ( participant : any ) => {
455- const sourceJid = `${ participant ?. id || participant ?. jid || participant ?. lid || '' } ` . trim ( )
456- const name = await resolveNameForJid ( phone , sourceJid )
538+ const { pnJid, waId, lid, responseJid } = await resolveParticipantIdentity ( phone , participant )
539+ const resolvedName = resolveParticipantNames ? await resolveParticipantName ( phone , participant , pnJid , lid ) : participantDisplayName ( participant )
540+ const name = firstNonEmptyString ( resolvedName , waId , lid ) || ''
457541 return {
458- jid : normalizeParticipantJidForResponse ( sourceJid ) ,
542+ jid : responseJid ,
543+ wa_id : waId ,
544+ user_id : lid ,
459545 name,
460546 }
461547 } ) )
0 commit comments