torn_api/
user.rs

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}