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 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 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 profile_view.skills = self.get_profile_skills(public_id, urn).await?;
82
83 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() } else if let Some(urn) = uniform_resource_name {
97 urn.id.clone() } 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 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 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 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() } else if let Some(urn) = uniform_resource_name {
194 urn.id.clone() } 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) = ¶ms.connection_of {
328 filters.push(format!("connectionOf->{connection_of}"));
329 }
330 if let Some(network_depth) = ¶ms.network_depth {
331 filters.push(format!("network->{network_depth}"));
332 }
333 if let Some(regions) = ¶ms.regions {
334 filters.push(format!("geoRegion->{}", regions.join("|")));
335 }
336 if let Some(industries) = ¶ms.industries {
337 filters.push(format!("industry->{}", industries.join("|")));
338 }
339 if let Some(current_company) = ¶ms.current_company {
340 filters.push(format!("currentCompany->{}", current_company.join("|")));
341 }
342 if let Some(past_companies) = ¶ms.past_companies {
343 filters.push(format!("pastCompany->{}", past_companies.join("|")));
344 }
345 if let Some(profile_languages) = ¶ms.profile_languages {
346 filters.push(format!("profileLanguage->{}", profile_languages.join("|")));
347 }
348 if let Some(nonprofit_interests) = ¶ms.nonprofit_interests {
349 filters.push(format!(
350 "nonprofitInterest->{}",
351 nonprofit_interests.join("|")
352 ));
353 }
354 if let Some(schools) = ¶ms.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) = ¶ms.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); }
647
648 if message_body.is_empty() {
649 return Ok(true); }
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); };
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}