1use serde::{
2 de::{self, MapAccess, Visitor},
3 Deserialize, Deserializer,
4};
5use std::{
6 collections::{BTreeMap, HashMap},
7 iter::zip,
8};
9
10use torn_api_macros::{ApiCategory, IntoOwned};
11
12use crate::de_util;
13
14pub use crate::common::{Attack, AttackFull, LastAction, Status};
15
16#[derive(Debug, Clone, Copy, ApiCategory)]
17#[api(category = "user")]
18#[non_exhaustive]
19pub enum UserSelection {
20 #[api(type = "Basic", flatten)]
21 Basic,
22 #[api(type = "Profile", flatten)]
23 Profile,
24 #[api(type = "Discord", field = "discord")]
25 Discord,
26 #[api(type = "PersonalStats", field = "personalstats")]
27 PersonalStats,
28 #[api(type = "CriminalRecord", field = "criminalrecord")]
29 Crimes,
30 #[api(type = "BTreeMap<i32, Attack>", field = "attacks")]
31 AttacksFull,
32 #[api(type = "BTreeMap<i32, AttackFull>", field = "attacks")]
33 Attacks,
34 #[api(type = "HashMap<Icon, &str>", field = "icons")]
35 Icons,
36 #[api(type = "Awards<Medals>", flatten)]
37 Medals,
38 #[api(type = "Awards<Honors>", flatten)]
39 Honors,
40}
41
42pub type Selection = UserSelection;
43
44#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Hash)]
45pub enum Gender {
46 Male,
47 Female,
48 Enby,
49}
50
51#[derive(Debug, IntoOwned)]
52pub struct Faction<'a> {
53 pub faction_id: i32,
54 pub faction_name: &'a str,
55 pub days_in_faction: i16,
56 pub position: &'a str,
57 pub faction_tag: Option<&'a str>,
58 pub faction_tag_image: Option<&'a str>,
59}
60
61fn deserialize_faction<'de, D>(deserializer: D) -> Result<Option<Faction<'de>>, D::Error>
62where
63 D: Deserializer<'de>,
64{
65 #[derive(Deserialize)]
66 #[serde(rename_all = "snake_case")]
67 enum Field {
68 FactionId,
69 FactionName,
70 DaysInFaction,
71 Position,
72 FactionTag,
73 FactionTagImage,
74 }
75
76 struct FactionVisitor;
77
78 impl<'de> Visitor<'de> for FactionVisitor {
79 type Value = Option<Faction<'de>>;
80
81 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
82 formatter.write_str("struct Faction")
83 }
84
85 fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
86 where
87 V: MapAccess<'de>,
88 {
89 let mut faction_id = None;
90 let mut faction_name = None;
91 let mut days_in_faction = None;
92 let mut position = None;
93 let mut faction_tag = None;
94 let mut faction_tag_image = None;
95
96 while let Some(key) = map.next_key()? {
97 match key {
98 Field::FactionId => {
99 faction_id = Some(map.next_value()?);
100 }
101 Field::FactionName => {
102 faction_name = Some(map.next_value()?);
103 }
104 Field::DaysInFaction => {
105 days_in_faction = Some(map.next_value()?);
106 }
107 Field::Position => {
108 position = Some(map.next_value()?);
109 }
110 Field::FactionTag => {
111 faction_tag = map.next_value()?;
112 }
113 Field::FactionTagImage => {
114 faction_tag_image = map.next_value()?;
115 }
116 }
117 }
118 let faction_id = faction_id.ok_or_else(|| de::Error::missing_field("faction_id"))?;
119 let faction_name =
120 faction_name.ok_or_else(|| de::Error::missing_field("faction_name"))?;
121 let days_in_faction =
122 days_in_faction.ok_or_else(|| de::Error::missing_field("days_in_faction"))?;
123 let position = position.ok_or_else(|| de::Error::missing_field("position"))?;
124
125 if faction_id == 0 {
126 Ok(None)
127 } else {
128 Ok(Some(Faction {
129 faction_id,
130 faction_name,
131 days_in_faction,
132 position,
133 faction_tag,
134 faction_tag_image,
135 }))
136 }
137 }
138 }
139
140 const FIELDS: &[&str] = &[
141 "faction_id",
142 "faction_name",
143 "days_in_faction",
144 "position",
145 "faction_tag",
146 ];
147 deserializer.deserialize_struct("Faction", FIELDS, FactionVisitor)
148}
149
150#[derive(Debug, IntoOwned, Deserialize)]
151pub struct Basic<'a> {
152 pub player_id: i32,
153 pub name: &'a str,
154 pub level: i16,
155 pub gender: Gender,
156 pub status: Status<'a>,
157}
158
159#[derive(Debug, Clone, IntoOwned, PartialEq, Eq, Deserialize)]
160#[into_owned(identity)]
161pub struct Discord {
162 #[serde(
163 rename = "userID",
164 deserialize_with = "de_util::empty_string_int_option"
165 )]
166 pub user_id: Option<i32>,
167 #[serde(rename = "discordID", deserialize_with = "de_util::string_is_long")]
168 pub discord_id: Option<i64>,
169}
170
171#[derive(Debug, Clone, Deserialize)]
172pub struct LifeBar {
173 pub current: i16,
174 pub maximum: i16,
175 pub increment: i16,
176}
177
178#[derive(Debug, Clone, Copy, Deserialize)]
179#[serde(rename_all = "kebab-case")]
180pub enum EliminationTeam2022 {
181 Firestarters,
182 HardBoiled,
183 QuackAddicts,
184 RainMen,
185 TotallyBoned,
186 RawringThunder,
187 DirtyCops,
188 LaughingStock,
189 JeanTherapy,
190 #[serde(rename = "satants-soldiers")]
191 SatansSoldiers,
192 WolfPack,
193 Sleepyheads,
194}
195
196#[derive(Debug, Clone, Copy, Deserialize)]
197#[serde(rename_all = "kebab-case")]
198pub enum EliminationTeam {
199 Backstabbers,
200 Cheese,
201 DeathsDoor,
202 RegularHumanPeople,
203 FlowerRangers,
204 ReligiousExtremists,
205 Hivemind,
206 CapsLockCrew,
207}
208
209#[derive(Debug, Clone, IntoOwned)]
210#[into_owned(identity)]
211pub enum Competition {
212 Elimination {
213 score: i32,
214 attacks: i16,
215 team: EliminationTeam,
216 },
217 DogTags {
218 score: i32,
219 position: Option<i32>,
220 },
221 Unknown,
222}
223
224fn deserialize_comp<'de, D>(deserializer: D) -> Result<Option<Competition>, D::Error>
225where
226 D: Deserializer<'de>,
227{
228 #[derive(Deserialize)]
229 #[serde(rename_all = "camelCase")]
230 enum Field {
231 Name,
232 Score,
233 Team,
234 Attacks,
235 TeamName,
236 Position,
237 #[serde(other)]
238 Ignore,
239 }
240
241 #[derive(Deserialize)]
242 enum CompetitionName {
243 Elimination,
244 #[serde(rename = "Dog Tags")]
245 DogTags,
246 #[serde(other)]
247 Unknown,
248 }
249
250 struct CompetitionVisitor;
251
252 impl<'de> Visitor<'de> for CompetitionVisitor {
253 type Value = Option<Competition>;
254
255 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
256 formatter.write_str("struct Competition")
257 }
258
259 fn visit_none<E>(self) -> Result<Self::Value, E>
260 where
261 E: de::Error,
262 {
263 Ok(None)
264 }
265
266 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
267 where
268 D: Deserializer<'de>,
269 {
270 deserializer.deserialize_map(self)
271 }
272
273 fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
274 where
275 V: MapAccess<'de>,
276 {
277 let mut team = None;
278 let mut score = None;
279 let mut attacks = None;
280 let mut name = None;
281 let mut position = None;
282
283 while let Some(key) = map.next_key()? {
284 match key {
285 Field::Name => {
286 name = Some(map.next_value()?);
287 }
288 Field::Score => {
289 score = Some(map.next_value()?);
290 }
291 Field::Attacks => {
292 attacks = Some(map.next_value()?);
293 }
294 Field::Position => {
295 position = Some(map.next_value()?);
296 }
297 Field::Team => {
298 let team_raw: &str = map.next_value()?;
299 team = if team_raw.is_empty() {
300 None
301 } else {
302 Some(match team_raw {
303 "backstabbers" => EliminationTeam::Backstabbers,
304 "cheese" => EliminationTeam::Cheese,
305 "deaths-door" => EliminationTeam::DeathsDoor,
306 "regular-human-people" => EliminationTeam::RegularHumanPeople,
307 "flower-rangers" => EliminationTeam::FlowerRangers,
308 "religious-extremists" => EliminationTeam::ReligiousExtremists,
309 "hivemind" => EliminationTeam::Hivemind,
310 "caps-lock-crew" => EliminationTeam::CapsLockCrew,
311 _ => Err(de::Error::unknown_variant(team_raw, &[]))?,
312 })
313 }
314 }
315 _ => (),
316 }
317 }
318
319 let name = name.ok_or_else(|| de::Error::missing_field("name"))?;
320
321 match name {
322 CompetitionName::Elimination => {
323 if let Some(team) = team {
324 let score = score.ok_or_else(|| de::Error::missing_field("score"))?;
325 let attacks = attacks.ok_or_else(|| de::Error::missing_field("attacks"))?;
326 Ok(Some(Competition::Elimination {
327 team,
328 score,
329 attacks,
330 }))
331 } else {
332 Ok(None)
333 }
334 }
335 CompetitionName::DogTags => {
336 let score = score.ok_or_else(|| de::Error::missing_field("score"))?;
337 let position = position.ok_or_else(|| de::Error::missing_field("position"))?;
338
339 Ok(Some(Competition::DogTags { score, position }))
340 }
341 CompetitionName::Unknown => Ok(Some(Competition::Unknown)),
342 }
343 }
344 }
345
346 deserializer.deserialize_option(CompetitionVisitor)
347}
348
349#[derive(Debug, IntoOwned, Deserialize)]
350pub struct Profile<'a> {
351 pub player_id: i32,
352 pub name: &'a str,
353 pub rank: &'a str,
354 pub level: i16,
355 pub gender: Gender,
356 pub age: i32,
357
358 pub life: LifeBar,
359 pub last_action: LastAction,
360 #[serde(deserialize_with = "deserialize_faction")]
361 pub faction: Option<Faction<'a>>,
362 pub job: EmploymentStatus,
363 pub status: Status<'a>,
364
365 #[serde(deserialize_with = "deserialize_comp")]
366 pub competition: Option<Competition>,
367
368 #[serde(deserialize_with = "de_util::int_is_bool")]
369 pub revivable: bool,
370}
371
372#[derive(Debug, Clone, Deserialize)]
373pub struct PersonalStats {
374 #[serde(rename = "attackswon")]
375 pub attacks_won: i32,
376 #[serde(rename = "attackslost")]
377 pub attacks_lost: i32,
378 #[serde(rename = "defendswon")]
379 pub defends_won: i32,
380 #[serde(rename = "defendslost")]
381 pub defends_lost: i32,
382 #[serde(rename = "statenhancersused")]
383 pub stat_enhancers_used: i32,
384 pub refills: i32,
385 #[serde(rename = "drugsused")]
386 pub drugs_used: i32,
387 #[serde(rename = "xantaken")]
388 pub xanax_taken: i32,
389 #[serde(rename = "lsdtaken")]
390 pub lsd_taken: i32,
391 #[serde(rename = "networth")]
392 pub net_worth: i64,
393 #[serde(rename = "energydrinkused")]
394 pub cans_used: i32,
395 #[serde(rename = "boostersused")]
396 pub boosters_used: i32,
397 pub awards: i16,
398 pub elo: i16,
399 #[serde(rename = "daysbeendonator")]
400 pub days_been_donator: i16,
401 #[serde(rename = "bestdamage")]
402 pub best_damage: i32,
403}
404
405#[derive(Deserialize)]
406pub struct Crimes1 {
407 pub selling_illegal_products: i32,
408 pub theft: i32,
409 pub auto_theft: i32,
410 pub drug_deals: i32,
411 pub computer_crimes: i32,
412 pub murder: i32,
413 pub fraud_crimes: i32,
414 pub other: i32,
415 pub total: i32,
416}
417
418#[derive(Deserialize)]
419pub struct Crimes2 {
420 pub vandalism: i32,
421 pub theft: i32,
422 pub counterfeiting: i32,
423 pub fraud: i32,
424 #[serde(rename = "illicitservices")]
425 pub illicit_services: i32,
426 #[serde(rename = "cybercrime")]
427 pub cyber_crime: i32,
428 pub extortion: i32,
429 #[serde(rename = "illegalproduction")]
430 pub illegal_production: i32,
431 pub total: i32,
432}
433
434#[derive(Deserialize)]
435#[serde(untagged)]
436pub enum CriminalRecord {
437 Crimes1(Crimes1),
438 Crimes2(Crimes2),
439}
440
441#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
442pub struct Icon(i16);
443
444impl Icon {
445 pub const SUBSCRIBER: Self = Self(4);
446 pub const LEVEL_100: Self = Self(5);
447 pub const GENDER_MALE: Self = Self(6);
448 pub const GENDER_FEMALE: Self = Self(7);
449 pub const MARITAL_STATUS: Self = Self(8);
450 pub const FACTION_MEMBER: Self = Self(9);
451 pub const PLAYER_COMMITTEE: Self = Self(10);
452 pub const STAFF: Self = Self(11);
453
454 pub const COMPANY: Self = Self(27);
455 pub const BANK_INVESTMENT: Self = Self(29);
456 pub const PROPERTY_VAULT: Self = Self(32);
457 pub const DUKE_LOAN: Self = Self(33);
458
459 pub const DRUG_COOLDOWN: Self = Self(53);
460
461 pub const FEDDED: Self = Self(70);
462 pub const TRAVELLING: Self = Self(71);
463 pub const FACTION_LEADER: Self = Self(74);
464 pub const TERRITORY_WAR: Self = Self(75);
465
466 pub const FACTION_RECRUIT: Self = Self(81);
467 pub const STOCK_MARKET: Self = Self(84);
468}
469
470impl<'de> Deserialize<'de> for Icon {
471 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
472 where
473 D: Deserializer<'de>,
474 {
475 struct IconVisitor;
476
477 impl<'de> Visitor<'de> for IconVisitor {
478 type Value = Icon;
479
480 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
481 write!(formatter, "struct Icon")
482 }
483
484 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
485 where
486 E: de::Error,
487 {
488 if let Some(suffix) = v.strip_prefix("icon") {
489 Ok(Icon(suffix.parse().map_err(|_e| {
490 de::Error::invalid_value(de::Unexpected::Str(suffix), &"&str \"IconXX\"")
491 })?))
492 } else {
493 Err(de::Error::invalid_value(
494 de::Unexpected::Str(v),
495 &"&str \"iconXX\"",
496 ))
497 }
498 }
499 }
500
501 deserializer.deserialize_str(IconVisitor)
502 }
503}
504
505#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Hash)]
506#[non_exhaustive]
507pub enum Job {
508 Director,
509 Employee,
510 Education,
511 Army,
512 Law,
513 Casino,
514 Medical,
515 Grocer,
516 #[serde(other)]
517 Other,
518}
519
520#[derive(Debug, Clone, PartialEq, Eq)]
521pub enum Company {
522 PlayerRun {
523 name: String,
524 id: i32,
525 company_type: u8,
526 },
527 CityJob,
528}
529
530impl<'de> Deserialize<'de> for Company {
531 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
532 where
533 D: Deserializer<'de>,
534 {
535 struct CompanyVisitor;
536
537 impl<'de> Visitor<'de> for CompanyVisitor {
538 type Value = Company;
539
540 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
541 formatter.write_str("enum Company")
542 }
543
544 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
545 where
546 A: MapAccess<'de>,
547 {
548 #[allow(clippy::enum_variant_names)]
549 #[derive(Deserialize)]
550 #[serde(rename_all = "snake_case")]
551 enum Field {
552 CompanyId,
553 CompanyName,
554 CompanyType,
555 #[serde(other)]
556 Other,
557 }
558
559 let mut id = None;
560 let mut name = None;
561 let mut company_type = None;
562
563 while let Some(key) = map.next_key()? {
564 match key {
565 Field::CompanyId => {
566 id = Some(map.next_value()?);
567 if id == Some(0) {
568 return Ok(Company::CityJob);
569 }
570 }
571 Field::CompanyType => company_type = Some(map.next_value()?),
572 Field::CompanyName => {
573 name = Some(map.next_value()?);
574 }
575 Field::Other => (),
576 }
577 }
578
579 let id = id.ok_or_else(|| de::Error::missing_field("company_id"))?;
580 let name = name.ok_or_else(|| de::Error::missing_field("company_name"))?;
581 let company_type =
582 company_type.ok_or_else(|| de::Error::missing_field("company_type"))?;
583
584 Ok(Company::PlayerRun {
585 name,
586 id,
587 company_type,
588 })
589 }
590 }
591
592 deserializer.deserialize_map(CompanyVisitor)
593 }
594}
595
596#[derive(Debug, Clone, Deserialize)]
597pub struct EmploymentStatus {
598 pub job: Job,
599 #[serde(flatten)]
600 pub company: Company,
601}
602
603#[derive(Debug, Clone, Copy)]
604pub struct Award {
605 pub award_time: chrono::DateTime<chrono::Utc>,
606}
607
608pub trait AwardMarker {
609 fn award_key() -> &'static str;
610 fn time_key() -> &'static str;
611}
612
613#[derive(Debug, Clone)]
614pub struct Awards<T>
615where
616 T: AwardMarker,
617{
618 pub inner: BTreeMap<i32, Award>,
619 marker: std::marker::PhantomData<T>,
620}
621
622impl<T> std::ops::Deref for Awards<T>
623where
624 T: AwardMarker,
625{
626 type Target = BTreeMap<i32, Award>;
627
628 fn deref(&self) -> &Self::Target {
629 &self.inner
630 }
631}
632
633impl<T> std::ops::DerefMut for Awards<T>
634where
635 T: AwardMarker,
636{
637 fn deref_mut(&mut self) -> &mut Self::Target {
638 &mut self.inner
639 }
640}
641
642impl<T> Awards<T>
643where
644 T: AwardMarker,
645{
646 pub fn into_inner(self) -> BTreeMap<i32, Award> {
647 self.inner
648 }
649}
650
651pub struct Medals;
652
653impl AwardMarker for Medals {
654 #[inline(always)]
655 fn award_key() -> &'static str {
656 "medals_awarded"
657 }
658 #[inline(always)]
659 fn time_key() -> &'static str {
660 "medals_time"
661 }
662}
663
664pub struct Honors;
665
666impl AwardMarker for Honors {
667 #[inline(always)]
668 fn award_key() -> &'static str {
669 "honors_awarded"
670 }
671 #[inline(always)]
672 fn time_key() -> &'static str {
673 "honors_time"
674 }
675}
676
677impl<'de, T> Deserialize<'de> for Awards<T>
678where
679 T: AwardMarker,
680{
681 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
682 where
683 D: Deserializer<'de>,
684 {
685 struct AwardVisitor<T>(std::marker::PhantomData<T>)
686 where
687 T: AwardMarker;
688
689 impl<'de, T> Visitor<'de> for AwardVisitor<T>
690 where
691 T: AwardMarker,
692 {
693 type Value = Awards<T>;
694
695 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
696 formatter.write_str("struct awards")
697 }
698
699 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
700 where
701 A: MapAccess<'de>,
702 {
703 let mut awards: Option<Vec<i32>> = None;
704 let mut times: Option<Vec<_>> = None;
705
706 while let Some(key) = map.next_key::<&'de str>()? {
707 if key == T::award_key() {
708 awards = map.next_value()?;
709 } else if key == T::time_key() {
710 times = map.next_value()?;
711 }
712 }
713
714 let awards = awards.ok_or_else(|| de::Error::missing_field(T::award_key()))?;
715 let times = times.ok_or_else(|| de::Error::missing_field(T::time_key()))?;
716
717 Ok(Awards {
718 inner: zip(
719 awards,
720 times.into_iter().map(|t| Award {
721 award_time: chrono::DateTime::from_timestamp(t, 0).unwrap_or_default(),
722 }),
723 )
724 .collect(),
725 marker: Default::default(),
726 })
727 }
728 }
729
730 deserializer.deserialize_map(AwardVisitor(Default::default()))
731 }
732}
733
734#[cfg(test)]
735mod tests {
736 use super::*;
737 use crate::tests::{async_test, setup, Client, ClientTrait};
738
739 #[async_test]
740 async fn user() {
741 let key = setup();
742
743 let response = Client::default()
744 .torn_api(key)
745 .user(|b| {
746 b.selections([
747 Selection::Basic,
748 Selection::Discord,
749 Selection::Profile,
750 Selection::PersonalStats,
751 Selection::Crimes,
752 Selection::Attacks,
753 Selection::Medals,
754 Selection::Honors,
755 ])
756 })
757 .await
758 .unwrap();
759
760 response.basic().unwrap();
761 response.discord().unwrap();
762 response.profile().unwrap();
763 response.personal_stats().unwrap();
764 response.crimes().unwrap();
765 response.attacks().unwrap();
766 response.attacks_full().unwrap();
767 response.medals().unwrap();
768 response.honors().unwrap();
769 }
770
771 #[async_test]
772 async fn not_in_faction() {
773 let key = setup();
774
775 let response = Client::default()
776 .torn_api(key)
777 .user(|b| b.id(28).selections([Selection::Profile]))
778 .await
779 .unwrap();
780
781 let faction = response.profile().unwrap().faction;
782
783 assert!(faction.is_none());
784 }
785
786 #[async_test]
787 async fn bulk() {
788 let key = setup();
789
790 let response = Client::default()
791 .torn_api(key)
792 .users([1, 2111649, 374272176892674048i64], |b| {
793 b.selections([Selection::Basic])
794 })
795 .await;
796
797 response.get(&1).as_ref().unwrap().as_ref().unwrap();
798 response.get(&2111649).as_ref().unwrap().as_ref().unwrap();
799 }
800
801 #[async_test]
802 async fn discord() {
803 let key = setup();
804
805 let response = Client::default()
806 .torn_api(key)
807 .user(|b| b.id(374272176892674048i64).selections([Selection::Basic]))
808 .await
809 .unwrap();
810
811 assert_eq!(response.basic().unwrap().player_id, 2111649);
812 }
813
814 #[async_test]
815 async fn fedded() {
816 let key = setup();
817
818 let response = Client::default()
819 .torn_api(key)
820 .user(|b| b.id(1900654).selections([Selection::Icons]))
821 .await
822 .unwrap();
823
824 let icons = response.icons().unwrap();
825
826 assert!(icons.contains_key(&Icon::FEDDED))
827 }
828}