linkedin_api/
linkedin.rs

1use serde_json::{json, Value};
2use std::collections::HashMap;
3use std::fs::write;
4use url::Url;
5use urlencoding::encode;
6
7use crate::client::Client;
8use crate::error::LinkedinError;
9use crate::types::ProfileView;
10use crate::{
11    types::Education, types::Experience, Company, Connection, ContactInfo, Conversation,
12    ConversationDetails, Identity, Invitation, MemberBadges, NetworkInfo, PersonSearchResult,
13    Profile, School, SearchPeopleParams, Skill, UniformResourceName,
14};
15
16const MAX_UPDATE_COUNT: usize = 100;
17const MAX_SEARCH_COUNT: usize = 49;
18const MAX_REPEATED_REQUESTS: usize = 200;
19
20#[derive(Clone)]
21pub struct LinkedinInner {
22    client: Client,
23}
24
25impl LinkedinInner {
26    pub async fn new(identity: &Identity, refresh_cookies: bool) -> Result<Self, LinkedinError> {
27        let client = Client::new()?;
28        client.authenticate(identity, refresh_cookies).await?;
29        Ok(Self { client })
30    }
31
32    pub async fn get_profile(
33        &self,
34        public_id: Option<&str>,
35        urn: Option<&UniformResourceName>,
36    ) -> Result<ProfileView, LinkedinError> {
37        let id = if let Some(pid) = public_id {
38            pid.to_string()
39        } else if let Some(urn) = urn {
40            urn.id.clone()
41        } else {
42            return Err(LinkedinError::InvalidInput(
43                "public_id or uniform_resource_name required".into(),
44            ));
45        };
46
47        let res = self
48            .client
49            .get(&format!("/identity/profiles/{id}/profileView"))
50            .await?;
51        if res.status() != 200 {
52            return Err(LinkedinError::RequestFailed(format!(
53                "status {}",
54                res.status()
55            )));
56        }
57
58        let data: serde_json::Value = res.json().await?;
59
60        let mut profile_view: ProfileView = serde_json::from_value(data)?;
61
62        // Derive helper fields not serialized directly
63        if let Some(mini) = &profile_view.profile.mini_profile {
64            if let Some(urn_str) = mini.entity_urn.as_deref() {
65                if let Ok(urn) = UniformResourceName::parse(urn_str) {
66                    profile_view.profile.profile_id = urn.id;
67                }
68            }
69        }
70
71        // Fill in profile_id
72        if let Some(mini) = &profile_view.profile.mini_profile {
73            if let Some(urn_str) = mini.entity_urn.as_deref() {
74                if let Ok(urn) = UniformResourceName::parse(urn_str) {
75                    profile_view.profile.profile_id = urn.id;
76                }
77            }
78        }
79
80        // Fill in skills (separate endpoint)
81        profile_view.skills = self.get_profile_skills(public_id, urn).await?;
82
83        // Fill in contact info (separate endpoint)
84        profile_view.profile.contact = self.get_profile_contact_info(public_id, urn).await?;
85
86        Ok(profile_view)
87    }
88
89    pub async fn get_profile_contact_info(
90        &self,
91        public_id: Option<&str>,
92        uniform_resource_name: Option<&UniformResourceName>,
93    ) -> Result<ContactInfo, LinkedinError> {
94        let id = if let Some(pid) = public_id {
95            pid.to_string() // use raw string
96        } else if let Some(urn) = uniform_resource_name {
97            urn.id.clone() // use strong type's .id
98        } else {
99            return Err(LinkedinError::InvalidInput(
100                "Either public_id or uniform_resource_name must be provided".into(),
101            ));
102        };
103
104        let res = self
105            .client
106            .get(&format!("/identity/profiles/{id}/profileContactInfo"))
107            .await?;
108
109        let data: Value = res.json().await?;
110
111        let mut contact_info = ContactInfo {
112            email_address: data
113                .get("emailAddress")
114                .and_then(|e| e.as_str())
115                .map(|s| s.parse().unwrap()),
116
117            websites: vec![],
118            twitter: vec![],
119            phone_numbers: vec![],
120            birthdate: data
121                .get("birthDateOn")
122                .and_then(|b| b.as_str())
123                .map(|s| s.parse().unwrap()),
124
125            ims: data.get("ims").map(|i| vec![i.clone()]),
126        };
127
128        // Parse websites
129        if let Some(websites) = data.get("websites").and_then(|w| w.as_array()) {
130            for website in websites {
131                let mut site = crate::types::Website {
132                    url: Some(
133                        website
134                            .get("url")
135                            .and_then(|u| u.as_str())
136                            .unwrap_or_default()
137                            .parse()
138                            .unwrap(),
139                    ),
140                    label: None,
141                };
142
143                if let Some(website_type) = website.get("type") {
144                    if let Some(standard) =
145                        website_type.get("com.linkedin.voyager.identity.profile.StandardWebsite")
146                    {
147                        site.label = standard
148                            .get("category")
149                            .and_then(|c| c.as_str())
150                            .map(|s| s.to_string());
151                    } else if let Some(custom) =
152                        website_type.get("com.linkedin.voyager.identity.profile.CustomWebsite")
153                    {
154                        site.label = custom
155                            .get("label")
156                            .and_then(|l| l.as_str())
157                            .map(|s| s.to_string());
158                    }
159                }
160
161                contact_info.websites.push(site);
162            }
163        }
164
165        // Parse Twitter handles
166        if let Some(twitter_handles) = data.get("twitterHandles").and_then(|t| t.as_array()) {
167            for handle in twitter_handles {
168                if let Some(name) = handle.get("name").and_then(|n| n.as_str()) {
169                    contact_info.twitter.push(name.to_string());
170                }
171            }
172        }
173
174        // Parse phone numbers
175        if let Some(phone_numbers) = data.get("phoneNumbers").and_then(|p| p.as_array()) {
176            for phone in phone_numbers {
177                if let Some(number) = phone.get("number").and_then(|n| n.as_str()) {
178                    contact_info.phone_numbers.push(number.parse().unwrap());
179                }
180            }
181        }
182
183        Ok(contact_info)
184    }
185
186    pub async fn get_profile_skills(
187        &self,
188        public_id: Option<&str>,
189        uniform_resource_name: Option<&UniformResourceName>,
190    ) -> Result<Vec<Skill>, LinkedinError> {
191        let id = if let Some(pid) = public_id {
192            pid.to_string() // use raw string
193        } else if let Some(urn) = uniform_resource_name {
194            urn.id.clone() // use strong type's .id
195        } else {
196            return Err(LinkedinError::InvalidInput(
197                "Either public_id or uniform_resource_name must be provided".into(),
198            ));
199        };
200
201        let res = self
202            .client
203            .get(&format!("/identity/profiles/{id}/skills?count=100&start=0"))
204            .await?;
205
206        let data: Value = res.json().await?;
207
208        let mut skills = vec![];
209
210        if let Some(elements) = data.get("elements").and_then(|e| e.as_array()) {
211            for element in elements {
212                if let Some(name) = element.get("name").and_then(|n| n.as_str()) {
213                    skills.push(Skill {
214                        entity_urn: None,
215                        name: name.to_string(),
216                    });
217                }
218            }
219        }
220
221        Ok(skills)
222    }
223
224    pub async fn get_profile_connections(
225        &self,
226        uniform_resource_name: &str,
227    ) -> Result<Vec<Connection>, LinkedinError> {
228        let params = SearchPeopleParams {
229            connection_of: Some(uniform_resource_name.to_string()),
230            network_depth: Some("F".to_string()),
231            ..Default::default()
232        };
233
234        let results = self.search_people(params).await?;
235
236        Ok(results
237            .into_iter()
238            .map(|r| Connection {
239                urn_id: r.urn_id,
240                public_id: r.public_id,
241                distance: r.distance,
242            })
243            .collect())
244    }
245
246    pub async fn search(
247        &self,
248        mut params: HashMap<String, String>,
249        limit: Option<usize>,
250    ) -> Result<Vec<Value>, LinkedinError> {
251        let count = limit.unwrap_or(MAX_SEARCH_COUNT).min(MAX_SEARCH_COUNT);
252
253        let default_params = vec![
254            ("count".to_string(), count.to_string()),
255            ("filters".to_string(), "List()".to_string()),
256            ("origin".to_string(), "GLOBAL_SEARCH_HEADER".to_string()),
257            ("q".to_string(), "all".to_string()),
258            ("start".to_string(), "0".to_string()),
259            ("queryContext".to_string(), "List(spellCorrectionEnabled->true,relatedSearchesEnabled->true,kcardTypes->PROFILE|COMPANY)".to_string()),
260        ];
261
262        for (key, value) in default_params {
263            params.entry(key).or_insert(value);
264        }
265
266        let mut results = vec![];
267        let mut start = 0;
268        let target_limit = limit.unwrap_or(usize::MAX);
269
270        loop {
271            params.insert("start".to_string(), start.to_string());
272
273            let query_string: String = params
274                .iter()
275                .map(|(k, v)| format!("{}={}", encode(k), encode(v)))
276                .collect::<Vec<_>>()
277                .join("&");
278
279            let res = self
280                .client
281                .get(&format!("/search/blended?{query_string}"))
282                .await?;
283            let data: Value = res.json().await?;
284
285            let mut new_elements = vec![];
286
287            if let Some(elements) = data
288                .get("data")
289                .and_then(|d| d.get("elements"))
290                .and_then(|e| e.as_array())
291            {
292                for element in elements {
293                    if let Some(inner_elements) = element.get("elements").and_then(|e| e.as_array())
294                    {
295                        new_elements.extend(inner_elements.iter().cloned());
296                    }
297                }
298            }
299
300            if new_elements.is_empty() {
301                break;
302            }
303
304            results.extend(
305                new_elements
306                    .iter()
307                    .take(target_limit.saturating_sub(results.len()))
308                    .cloned(),
309            );
310
311            if results.len() >= target_limit || results.len() / count >= MAX_REPEATED_REQUESTS {
312                break;
313            }
314
315            start += count;
316        }
317
318        Ok(results.into_iter().take(target_limit).collect())
319    }
320
321    pub async fn search_people(
322        &self,
323        params: SearchPeopleParams,
324    ) -> Result<Vec<PersonSearchResult>, LinkedinError> {
325        let mut filters = vec!["resultType->PEOPLE".to_string()];
326
327        if let Some(connection_of) = &params.connection_of {
328            filters.push(format!("connectionOf->{connection_of}"));
329        }
330        if let Some(network_depth) = &params.network_depth {
331            filters.push(format!("network->{network_depth}"));
332        }
333        if let Some(regions) = &params.regions {
334            filters.push(format!("geoRegion->{}", regions.join("|")));
335        }
336        if let Some(industries) = &params.industries {
337            filters.push(format!("industry->{}", industries.join("|")));
338        }
339        if let Some(current_company) = &params.current_company {
340            filters.push(format!("currentCompany->{}", current_company.join("|")));
341        }
342        if let Some(past_companies) = &params.past_companies {
343            filters.push(format!("pastCompany->{}", past_companies.join("|")));
344        }
345        if let Some(profile_languages) = &params.profile_languages {
346            filters.push(format!("profileLanguage->{}", profile_languages.join("|")));
347        }
348        if let Some(nonprofit_interests) = &params.nonprofit_interests {
349            filters.push(format!(
350                "nonprofitInterest->{}",
351                nonprofit_interests.join("|")
352            ));
353        }
354        if let Some(schools) = &params.schools {
355            filters.push(format!("schools->{}", schools.join("|")));
356        }
357
358        let mut search_params = HashMap::new();
359        search_params.insert(
360            "filters".to_string(),
361            format!("List({})", filters.join(",")),
362        );
363
364        if let Some(keywords) = &params.keywords {
365            search_params.insert("keywords".to_string(), keywords.clone());
366        }
367
368        let data = self.search(search_params, params.limit).await?;
369
370        let mut results = vec![];
371        for item in data {
372            if let Some(public_id) = item.get("publicIdentifier").and_then(|p| p.as_str()) {
373                let urn_id = item
374                    .get("targetUrn")
375                    .and_then(|u| u.as_str())
376                    .and_then(|s| UniformResourceName::parse(s).ok())
377                    .map(|urn| urn.id)
378                    .unwrap_or_default();
379                let distance = item
380                    .get("memberDistance")
381                    .and_then(|d| d.get("value"))
382                    .and_then(|v| v.as_str())
383                    .unwrap_or("");
384
385                results.push(PersonSearchResult {
386                    urn_id: urn_id.to_string(),
387                    public_id: public_id.to_string(),
388                    distance: distance.to_string(),
389                });
390            }
391        }
392
393        Ok(results)
394    }
395
396    pub async fn get_company_updates(
397        &self,
398        public_id: Option<&str>,
399        uniform_resource_name: Option<&str>,
400        max_results: Option<usize>,
401    ) -> Result<Vec<Value>, LinkedinError> {
402        let id = public_id.or(uniform_resource_name).ok_or_else(|| {
403            LinkedinError::InvalidInput(
404                "Either public_id or uniform_resource_name must be provided".to_string(),
405            )
406        })?;
407
408        let mut results = vec![];
409        let mut start = 0;
410        let max_results = max_results.unwrap_or(usize::MAX);
411
412        loop {
413            let params = format!("?companyUniversalName={id}&q=companyFeedByUniversalName&moduleKey=member-share&count={MAX_UPDATE_COUNT}&start={start}");
414
415            let res = self.client.get(&format!("/feed/updates{params}")).await?;
416            let data: Value = res.json().await?;
417
418            if let Some(elements) = data.get("elements").and_then(|e| e.as_array()) {
419                if elements.is_empty()
420                    || results.len() >= max_results
421                    || results.len() / MAX_UPDATE_COUNT >= MAX_REPEATED_REQUESTS
422                {
423                    break;
424                }
425
426                results.extend(
427                    elements
428                        .iter()
429                        .take(max_results.saturating_sub(results.len()))
430                        .cloned(),
431                );
432                start += MAX_UPDATE_COUNT;
433            } else {
434                break;
435            }
436        }
437
438        Ok(results)
439    }
440
441    pub async fn get_profile_updates(
442        &self,
443        public_id: Option<&str>,
444        uniform_resource_name: Option<&str>,
445        max_results: Option<usize>,
446    ) -> Result<Vec<Value>, LinkedinError> {
447        let id = public_id.or(uniform_resource_name).ok_or_else(|| {
448            LinkedinError::InvalidInput(
449                "Either public_id or uniform_resource_name must be provided".to_string(),
450            )
451        })?;
452
453        let mut results = vec![];
454        let mut start = 0;
455        let max_results = max_results.unwrap_or(usize::MAX);
456
457        loop {
458            let params = format!(
459                "?profileId={id}&q=memberShareFeed&moduleKey=member-share&count={MAX_UPDATE_COUNT}&start={start}"
460            );
461
462            let res = self.client.get(&format!("/feed/updates{params}")).await?;
463            let data: Value = res.json().await?;
464
465            if let Some(elements) = data.get("elements").and_then(|e| e.as_array()) {
466                if elements.is_empty()
467                    || results.len() >= max_results
468                    || results.len() / MAX_UPDATE_COUNT >= MAX_REPEATED_REQUESTS
469                {
470                    break;
471                }
472
473                results.extend(
474                    elements
475                        .iter()
476                        .take(max_results.saturating_sub(results.len()))
477                        .cloned(),
478                );
479                start += MAX_UPDATE_COUNT;
480            } else {
481                break;
482            }
483        }
484
485        Ok(results)
486    }
487
488    pub async fn get_current_profile_views(&self) -> Result<u64, LinkedinError> {
489        let res = self.client.get("/identity/wvmpCards").await?;
490        let data: Value = res.json().await?;
491
492        let views = data
493            .get("elements")
494            .and_then(|e| e.get(0))
495            .and_then(|e| e.get("value"))
496            .and_then(|v| v.get("com.linkedin.voyager.identity.me.wvmpOverview.WvmpViewersCard"))
497            .and_then(|c| c.get("insightCards"))
498            .and_then(|i| i.get(0))
499            .and_then(|i| i.get("value"))
500            .and_then(|v| {
501                v.get("com.linkedin.voyager.identity.me.wvmpOverview.WvmpSummaryInsightCard")
502            })
503            .and_then(|s| s.get("numViews"))
504            .and_then(|n| n.as_u64())
505            .unwrap_or(0);
506
507        Ok(views)
508    }
509
510    pub async fn get_school(&self, public_id: &str) -> Result<School, LinkedinError> {
511        let params = format!("?decorationId=com.linkedin.voyager.deco.organization.web.WebFullCompanyMain-12&q=universalName&universalName={public_id}");
512
513        let res = self
514            .client
515            .get(&format!("/organization/companies{params}"))
516            .await?;
517        let data: Value = res.json().await?;
518
519        if let Some(status) = data.get("status") {
520            if status != 200 {
521                return Err(LinkedinError::RequestFailed(
522                    "School request failed".to_string(),
523                ));
524            }
525        }
526
527        let school_data = data
528            .get("elements")
529            .and_then(|e| e.get(0))
530            .ok_or_else(|| LinkedinError::RequestFailed("No school data found".to_string()))?;
531
532        let name = school_data
533            .get("name")
534            .and_then(|n| n.as_str())
535            .ok_or_else(|| LinkedinError::RequestFailed("No school name found".to_string()))?;
536
537        Ok(School {
538            name: name.to_string(),
539        })
540    }
541
542    pub async fn get_company(&self, public_id: &str) -> Result<Company, LinkedinError> {
543        let params = format!("?decorationId=com.linkedin.voyager.deco.organization.web.WebFullCompanyMain-12&q=universalName&universalName={public_id}");
544
545        let res = self
546            .client
547            .get(&format!("/organization/companies{params}"))
548            .await?;
549        let data: Value = res.json().await?;
550
551        if let Some(status) = data.get("status") {
552            if status != 200 {
553                return Err(LinkedinError::RequestFailed(
554                    data.get("message")
555                        .unwrap_or(&Value::String("Unknown error".to_string()))
556                        .as_str()
557                        .unwrap()
558                        .to_string(),
559                ));
560            }
561        }
562
563        let company_data = data
564            .get("elements")
565            .and_then(|e| e.get(0))
566            .ok_or_else(|| LinkedinError::RequestFailed("No company data found".to_string()))?;
567
568        let name = company_data
569            .get("name")
570            .and_then(|n| n.as_str())
571            .ok_or_else(|| LinkedinError::RequestFailed("No company name found".to_string()))?;
572
573        Ok(Company {
574            name: name.to_string(),
575        })
576    }
577
578    pub async fn get_conversation_details(
579        &self,
580        profile_uniform_resource_name: &str,
581    ) -> Result<ConversationDetails, LinkedinError> {
582        let res = self.client.get(&format!("/messaging/conversations?keyVersion=LEGACY_INBOX&q=participants&recipients=List({profile_uniform_resource_name})")).await?;
583        let data: Value = res.json().await?;
584
585        let item = data
586            .get("elements")
587            .and_then(|e| e.get(0))
588            .ok_or_else(|| LinkedinError::RequestFailed("No conversation found".to_string()))?;
589
590        let entity_urn = item
591            .get("entityUrn")
592            .and_then(|u| u.as_str())
593            .ok_or(LinkedinError::RequestFailed("No entityUrn".into()))?;
594        let urn = UniformResourceName::parse(entity_urn)?;
595        let id = urn.id;
596
597        Ok(ConversationDetails { id: id.to_string() })
598    }
599
600    pub async fn get_conversations(&self) -> Result<Vec<Conversation>, LinkedinError> {
601        let res = self
602            .client
603            .get("/messaging/conversations?keyVersion=LEGACY_INBOX")
604            .await?;
605        let data: Value = res.json().await?;
606
607        let mut conversations = vec![];
608
609        if let Some(elements) = data.get("elements").and_then(|e| e.as_array()) {
610            for element in elements {
611                if let Some(entity_urn) = element.get("entityUrn").and_then(|u| u.as_str()) {
612                    let id = UniformResourceName::parse(entity_urn).unwrap().id;
613                    conversations.push(Conversation { id });
614                }
615            }
616        }
617
618        Ok(conversations)
619    }
620
621    pub async fn get_conversation(
622        &self,
623        conversation_uniform_resource_name: &str,
624    ) -> Result<Conversation, LinkedinError> {
625        let res = self
626            .client
627            .get(&format!(
628                "/messaging/conversations/{conversation_uniform_resource_name}/events"
629            ))
630            .await?;
631        let _data: Value = res.json().await?;
632
633        Ok(Conversation {
634            id: conversation_uniform_resource_name.to_string(),
635        })
636    }
637
638    pub async fn send_message(
639        &self,
640        conversation_uniform_resource_name: Option<&str>,
641        recipients: Option<Vec<String>>,
642        message_body: &str,
643    ) -> Result<bool, LinkedinError> {
644        if conversation_uniform_resource_name.is_none() && recipients.is_none() {
645            return Ok(true); // Error case
646        }
647
648        if message_body.is_empty() {
649            return Ok(true); // Error case
650        }
651
652        let message_event = json!({
653            "eventCreate": {
654                "value": {
655                    "com.linkedin.voyager.messaging.create.MessageCreate": {
656                        "body": message_body,
657                        "attachments": [],
658                        "attributedBody": {
659                            "text": message_body,
660                            "attributes": []
661                        },
662                        "mediaAttachments": []
663                    }
664                }
665            }
666        });
667
668        let res = if let Some(conv_id) = conversation_uniform_resource_name {
669            self.client
670                .post(
671                    &format!("/messaging/conversations/{conv_id}/events?action=create"),
672                    &message_event,
673                )
674                .await?
675        } else if let Some(recips) = recipients {
676            let mut payload = message_event;
677            payload["recipients"] = json!(recips);
678            payload["subtype"] = json!("MEMBER_TO_MEMBER");
679
680            let full_payload = json!({
681                "keyVersion": "LEGACY_INBOX",
682                "conversationCreate": payload
683            });
684
685            self.client
686                .post("/messaging/conversations?action=create", &full_payload)
687                .await?
688        } else {
689            return Ok(true); // Error case
690        };
691
692        Ok(res.status() != 201)
693    }
694
695    pub async fn mark_conversation_as_seen(
696        &self,
697        conversation_uniform_resource_name: &str,
698    ) -> Result<bool, LinkedinError> {
699        let payload = json!({
700            "patch": {
701                "$set": {
702                    "read": true
703                }
704            }
705        });
706
707        let res = self
708            .client
709            .post(
710                &format!("/messaging/conversations/{conversation_uniform_resource_name}"),
711                &payload,
712            )
713            .await?;
714        Ok(res.status() != 200)
715    }
716
717    pub async fn get_user_profile(&self) -> Result<Value, LinkedinError> {
718        crate::utils::evade().await;
719        let res = self.client.get("/me").await?;
720        res.json().await.map_err(Into::into)
721    }
722
723    pub async fn get_invitations(
724        &self,
725        start: usize,
726        limit: usize,
727    ) -> Result<Vec<Invitation>, LinkedinError> {
728        let params =
729            format!("?start={start}&count={limit}&includeInsights=true&q=receivedInvitation");
730
731        let res = self
732            .client
733            .get(&format!("/relationships/invitationViews{params}"))
734            .await?;
735
736        if res.status() != 200 {
737            return Ok(vec![]);
738        }
739
740        let data: Value = res.json().await?;
741
742        let mut invitations = vec![];
743
744        if let Some(elements) = data.get("elements").and_then(|e| e.as_array()) {
745            for element in elements {
746                if let Some(invitation) = element.get("invitation") {
747                    if let (Some(entity_urn), Some(shared_secret)) = (
748                        invitation.get("entityUrn").and_then(|u| u.as_str()),
749                        invitation.get("sharedSecret").and_then(|s| s.as_str()),
750                    ) {
751                        invitations.push(Invitation {
752                            entity_urn: Some(entity_urn.to_string()),
753                            shared_secret: shared_secret.to_string(),
754                        });
755                    }
756                }
757            }
758        }
759
760        Ok(invitations)
761    }
762
763    pub async fn reply_invitation(
764        &self,
765        invitation_entity_urn: &str,
766        invitation_shared_secret: &str,
767        action: &str,
768    ) -> Result<bool, LinkedinError> {
769        let urn = UniformResourceName::parse(invitation_entity_urn)?;
770
771        let payload = json!({
772            "invitationId": urn.id,
773            "invitationSharedSecret": invitation_shared_secret,
774            "isGenericInvitation": false
775        });
776
777        let invitation_id = urn.id;
778        let res = self
779            .client
780            .post(
781                &format!("/relationships/invitations/{invitation_id}?action={action}"),
782                &payload,
783            )
784            .await?;
785
786        Ok(res.status() == 200)
787    }
788
789    pub async fn remove_connection(&self, public_profile_id: &str) -> Result<bool, LinkedinError> {
790        let res = self
791            .client
792            .post(
793                &format!("/identity/profiles/{public_profile_id}/profileActions?action=disconnect"),
794                &json!({}),
795            )
796            .await?;
797
798        Ok(res.status() != 200)
799    }
800
801    pub async fn get_profile_privacy_settings(
802        &self,
803        public_profile_id: &str,
804    ) -> Result<HashMap<String, Value>, LinkedinError> {
805        let res = self
806            .client
807            .get(&format!(
808                "/identity/profiles/{public_profile_id}/privacySettings"
809            ))
810            .await?;
811
812        if res.status() != 200 {
813            return Ok(HashMap::new());
814        }
815
816        let data: Value = res.json().await?;
817
818        if let Some(data_obj) = data.get("data").and_then(|d| d.as_object()) {
819            Ok(data_obj
820                .iter()
821                .map(|(k, v)| (k.clone(), v.clone()))
822                .collect())
823        } else {
824            Ok(HashMap::new())
825        }
826    }
827
828    pub async fn get_profile_member_badges(
829        &self,
830        public_profile_id: &str,
831    ) -> Result<MemberBadges, LinkedinError> {
832        let res = self
833            .client
834            .get(&format!(
835                "/identity/profiles/{public_profile_id}/memberBadges"
836            ))
837            .await?;
838
839        if res.status() != 200 {
840            return Ok(MemberBadges {
841                premium: false,
842                open_link: false,
843                influencer: false,
844                job_seeker: false,
845            });
846        }
847
848        let data: Value = res.json().await?;
849
850        let empty_map = Value::Object(serde_json::Map::new());
851        let badges_data = data.get("data").unwrap_or(&empty_map);
852
853        Ok(MemberBadges {
854            premium: badges_data
855                .get("premium")
856                .and_then(|p| p.as_bool())
857                .unwrap_or(false),
858            open_link: badges_data
859                .get("openLink")
860                .and_then(|o| o.as_bool())
861                .unwrap_or(false),
862            influencer: badges_data
863                .get("influencer")
864                .and_then(|i| i.as_bool())
865                .unwrap_or(false),
866            job_seeker: badges_data
867                .get("jobSeeker")
868                .and_then(|j| j.as_bool())
869                .unwrap_or(false),
870        })
871    }
872
873    pub async fn get_profile_network_info(
874        &self,
875        public_profile_id: &str,
876    ) -> Result<NetworkInfo, LinkedinError> {
877        let res = self
878            .client
879            .get(&format!(
880                "/identity/profiles/{public_profile_id}/networkinfo"
881            ))
882            .await?;
883
884        if res.status() != 200 {
885            return Ok(NetworkInfo { followers_count: 0 });
886        }
887
888        let data: Value = res.json().await?;
889
890        let followers_count = data
891            .get("data")
892            .and_then(|d| d.get("followersCount"))
893            .and_then(|f| f.as_u64())
894            .unwrap_or(0);
895
896        Ok(NetworkInfo { followers_count })
897    }
898
899    pub async fn stub_people_search(
900        &self,
901        query: &str,
902        count: usize,
903        start: usize,
904    ) -> Result<Value, LinkedinError> {
905        let encoded_query = encode(query);
906
907        let mut url = format!("/search/hits?count={count}&guides=List%28v-%253EPEOPLE%29&keywords={encoded_query}&origin=SWITCH_SEARCH_VERTICAL&q=guided");
908
909        if start > 0 {
910            url.push_str(&format!("&start={start}"));
911        }
912
913        let res = self.client.get(&url).await?;
914
915        if res.status() != 200 {
916            return Ok(json!({}));
917        }
918
919        res.json().await.map_err(Into::into)
920    }
921}