@@ -97,9 +97,21 @@ export interface CreateGroupInput {
9797 industry ?: string ;
9898 location ?: string ;
9999 isUnlisted ?: boolean ;
100+ logoPath ?: string ;
101+ coverImagePath ?: string ;
100102 operatorNote ?: string ;
101103}
102104
105+ export interface ListGroupsInput {
106+ profileName ?: string ;
107+ limit ?: number ;
108+ }
109+
110+ export interface ListGroupsOutput {
111+ results : LinkedInGroupsSearchResult [ ] ;
112+ count : number ;
113+ }
114+
103115export interface SearchGroupsInput {
104116 profileName ?: string ;
105117 query : string ;
@@ -919,7 +931,7 @@ export class CreateGroupActionExecutor
919931
920932 const group = await runtime . profileManager . runWithPersistentContext (
921933 data . profileName ?? "default" ,
922- { headless : false } ,
934+ { headless : true } ,
923935 async ( context ) => {
924936 const page = await context . newPage ( ) ;
925937 try {
@@ -943,6 +955,47 @@ export class CreateGroupActionExecutor
943955 await page . locator ( "label[for='unlisted-group']" ) . click ( ) ;
944956 }
945957
958+ if ( data . industry ) {
959+ const industryLocator = page . locator ( "input[placeholder*='Industry']" ) . or ( page . getByLabel ( / I n d u s t r y / i) ) . first ( ) ;
960+ await industryLocator . fill ( data . industry ) ;
961+ await page . waitForTimeout ( 1000 ) ;
962+ await page . keyboard . press ( "ArrowDown" ) ;
963+ await page . keyboard . press ( "Enter" ) ;
964+ }
965+
966+ if ( data . location ) {
967+ const locationLocator = page . locator ( "input[placeholder*='Location']" ) . or ( page . getByLabel ( / L o c a t i o n / i) ) . first ( ) ;
968+ await locationLocator . fill ( data . location ) ;
969+ await page . waitForTimeout ( 1000 ) ;
970+ await page . keyboard . press ( "ArrowDown" ) ;
971+ await page . keyboard . press ( "Enter" ) ;
972+ }
973+
974+ if ( data . logoPath ) {
975+ const fileChooserPromise = page . waitForEvent ( 'filechooser' ) ;
976+ const logoButton = page . locator ( "button[aria-label*='logo']" ) . or ( page . getByRole ( "button" , { name : / l o g o / i } ) ) . first ( ) ;
977+ if ( await logoButton . isVisible ( ) . catch ( ( ) => false ) ) {
978+ await logoButton . click ( ) ;
979+ const fileChooser = await fileChooserPromise ;
980+ await fileChooser . setFiles ( data . logoPath ) ;
981+ await page . waitForTimeout ( 1000 ) ;
982+ await page . getByRole ( "button" , { name : / A p p l y | S a v e / i } ) . first ( ) . click ( ) . catch ( ( ) => { } ) ;
983+ }
984+ }
985+
986+ if ( data . coverImagePath ) {
987+ const fileChooserPromise = page . waitForEvent ( 'filechooser' ) ;
988+ const coverButton = page . locator ( "button[aria-label*='cover image']" ) . or ( page . getByRole ( "button" , { name : / c o v e r i m a g e / i } ) ) . first ( ) ;
989+ if ( await coverButton . isVisible ( ) . catch ( ( ) => false ) ) {
990+ await coverButton . click ( ) ;
991+ const fileChooser = await fileChooserPromise ;
992+ await fileChooser . setFiles ( data . coverImagePath ) ;
993+ await page . waitForTimeout ( 1000 ) ;
994+ await page . getByRole ( "button" , { name : / A p p l y | S a v e / i } ) . first ( ) . click ( ) . catch ( ( ) => { } ) ;
995+ }
996+ }
997+
998+
946999 await page . locator ( "button[type='submit']" ) . click ( ) ;
9471000
9481001 // Wait for redirect to new group page
@@ -1071,6 +1124,8 @@ export class LinkedInGroupsService {
10711124 industry : input . industry ,
10721125 location : input . location ,
10731126 isUnlisted : input . isUnlisted ,
1127+ logoPath : input . logoPath ,
1128+ coverImagePath : input . coverImagePath ,
10741129 } ,
10751130 preview : {
10761131 summary : `Create LinkedIn group "${ input . name } "` ,
@@ -1087,6 +1142,73 @@ export class LinkedInGroupsService {
10871142 } ) ;
10881143 }
10891144
1145+
1146+ async listGroups ( input : ListGroupsInput ) : Promise < ListGroupsOutput > {
1147+ const profileName = input . profileName ?? "default" ;
1148+ const limit = readSearchLimit ( input . limit ) ;
1149+
1150+ await this . runtime . auth . ensureAuthenticated ( {
1151+ profileName
1152+ } ) ;
1153+
1154+ try {
1155+ const snapshots = await this . runtime . profileManager . runWithPersistentContext (
1156+ profileName ,
1157+ { headless : true } ,
1158+ async ( context ) => {
1159+ const page = await getOrCreatePage ( context ) ;
1160+ await page . goto ( "https://www.linkedin.com/groups/" , {
1161+ waitUntil : "domcontentloaded"
1162+ } ) ;
1163+ await waitForNetworkIdleBestEffort ( page ) ;
1164+
1165+ const selectors = [
1166+ ".groups-list" ,
1167+ "ul.groups-list" ,
1168+ ".scaffold-layout__main" ,
1169+ "main"
1170+ ] ;
1171+ for ( const selector of selectors ) {
1172+ try {
1173+ await page . locator ( selector ) . first ( ) . waitFor ( {
1174+ state : "visible" ,
1175+ timeout : 5_000
1176+ } ) ;
1177+ break ;
1178+ } catch {
1179+ // Try next
1180+ }
1181+ }
1182+ return scrollSearchResultsIfNeeded ( page , extractGroupSearchResults , limit ) ;
1183+ }
1184+ ) ;
1185+
1186+ const results = snapshots
1187+ . map ( ( snapshot ) => ( {
1188+ group_id : normalizeText ( snapshot . group_id ) ,
1189+ name : normalizeText ( snapshot . name ) ,
1190+ group_url : normalizeText ( snapshot . group_url ) ,
1191+ visibility : normalizeText ( snapshot . visibility ) ,
1192+ member_count : normalizeText ( snapshot . member_count ) ,
1193+ description : normalizeText ( snapshot . description ) ,
1194+ membership_state : snapshot . membership_state
1195+ } ) )
1196+ . filter ( ( result ) => result . name . length > 0 || result . group_url . length > 0 )
1197+ . slice ( 0 , limit ) ;
1198+
1199+ return {
1200+ results,
1201+ count : results . length
1202+ } ;
1203+ } catch ( error ) {
1204+ throw asLinkedInBuddyError (
1205+ error ,
1206+ "UNKNOWN" ,
1207+ "Failed to list LinkedIn groups."
1208+ ) ;
1209+ }
1210+ }
1211+
10901212 async searchGroups ( input : SearchGroupsInput ) : Promise < SearchGroupsOutput > {
10911213 const profileName = input . profileName ?? "default" ;
10921214 const query = normalizeText ( input . query ) ;
0 commit comments